mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
585 lines
24 KiB
Python
585 lines
24 KiB
Python
# "The contents of this file are subject to the Common Public Attribution
|
|
# License Version 1.0. (the "License"); you may not use this file except in
|
|
# compliance with the License. You may obtain a copy of the License at
|
|
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
|
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
|
# software over a computer network and provide for limited attribution for the
|
|
# Original Developer. In addition, Exhibit A has been modified to be consistent
|
|
# with Exhibit B.
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS" basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
|
# the specific language governing rights and limitations under the License.
|
|
#
|
|
# The Original Code is Reddit.
|
|
#
|
|
# The Original Developer is the Initial Developer. The Initial Developer of the
|
|
# Original Code is CondeNet, Inc.
|
|
#
|
|
# All portions of the code written by CondeNet are Copyright (c) 2006-2008
|
|
# CondeNet, Inc. All Rights Reserved.
|
|
################################################################################
|
|
from validator import *
|
|
from pylons.i18n import _, ungettext
|
|
from reddit_base import RedditController, base_listing
|
|
from api import link_listing_by_url
|
|
from r2 import config
|
|
from r2.models import *
|
|
from r2.lib.pages import *
|
|
from r2.lib.menus import *
|
|
from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url, query_string
|
|
from r2.lib.emailer import opt_out, opt_in
|
|
from r2.lib.db.operators import desc
|
|
from r2.lib.strings import strings
|
|
import r2.lib.db.thing as thing
|
|
from listingcontroller import ListingController
|
|
from pylons import c, request
|
|
|
|
import random as rand
|
|
import re
|
|
from urllib import quote_plus
|
|
|
|
from admin import admin_profile_query
|
|
|
|
class FrontController(RedditController):
|
|
|
|
@validate(article = VLink('article'),
|
|
comment = VCommentID('comment'))
|
|
def GET_oldinfo(self, article, type, dest, rest=None, comment=''):
|
|
"""Legacy: supporting permalink pages from '06,
|
|
and non-search-engine-friendly links"""
|
|
if not (dest in ('comments','related','details')):
|
|
dest = 'comments'
|
|
if type == 'ancient':
|
|
#this could go in config, but it should never change
|
|
max_link_id = 10000000
|
|
new_id = max_link_id - int(article._id)
|
|
return self.redirect('/info/' + to36(new_id) + '/' + rest)
|
|
if type == 'old':
|
|
new_url = "/%s/%s/%s" % \
|
|
(dest, article._id36,
|
|
quote_plus(title_to_url(article.title).encode('utf-8')))
|
|
if not c.default_sr:
|
|
new_url = "/r/%s%s" % (c.site.name, new_url)
|
|
if comment:
|
|
new_url = new_url + "/%s" % comment._id36
|
|
if request.environ.get('extension'):
|
|
new_url = new_url + "/.%s" % request.environ.get('extension')
|
|
|
|
new_url = new_url + query_string(request.get)
|
|
|
|
# redirect should be smarter and handle extentions, etc.
|
|
return self.redirect(new_url)
|
|
|
|
def GET_random(self):
|
|
"""The Serendipity button"""
|
|
n = rand.randint(0, 9)
|
|
links = Link._query(*c.site.query_rules())
|
|
links._sort = desc('_date') if n > 5 else desc('_hot')
|
|
links._limit = 50
|
|
links = list(links)
|
|
l = links[rand.randint(0, len(links)-1)]
|
|
l._load()
|
|
return self.redirect(l.url)
|
|
|
|
def GET_password(self):
|
|
"""The 'what is my password' page"""
|
|
return BoringPage(_("password"), content=Password()).render()
|
|
|
|
@validate(user = VCacheKey('reset', ('key', 'name')),
|
|
key = nop('key'))
|
|
def GET_resetpassword(self, user, key):
|
|
"""page hit once a user has been sent a password reset email
|
|
to verify their identity before allowing them to update their
|
|
password."""
|
|
done = False
|
|
if not key and request.referer:
|
|
referer_path = request.referer.split(c.domain)[-1]
|
|
done = referer_path.startswith(request.fullpath)
|
|
elif not user:
|
|
return self.abort404()
|
|
return BoringPage(_("reset password"),
|
|
content=ResetPassword(key=key, done=done)).render()
|
|
|
|
@validate(VAdmin(),
|
|
article = VLink('article'))
|
|
def GET_details(self, article):
|
|
"""The (now depricated) details page. Content on this page
|
|
has been subsubmed by the presence of the LinkInfoBar on the
|
|
rightbox, so it is only useful for Admin-only wizardry."""
|
|
return DetailsPage(link = article).render()
|
|
|
|
|
|
@validate(article = VLink('article'),
|
|
comment = VCommentID('comment'),
|
|
context = VInt('context', min = 0, max = 8),
|
|
sort = VMenu('controller', CommentSortMenu),
|
|
num_comments = VMenu('controller', NumCommentsMenu))
|
|
def GET_comments(self, article, comment, context, sort, num_comments):
|
|
"""Comment page for a given 'article'."""
|
|
if comment and comment.link_id != article._id:
|
|
return self.abort404()
|
|
|
|
if not c.default_sr and c.site._id != article.sr_id:
|
|
return self.abort404()
|
|
|
|
#check for 304
|
|
self.check_modified(article, 'comments')
|
|
|
|
# if there is a focal comment, communicate down to comment_skeleton.html who
|
|
# that will be
|
|
if comment:
|
|
c.focal_comment = comment._id36
|
|
|
|
# check if we just came from the submit page
|
|
infotext = None
|
|
if request.get.get('already_submitted'):
|
|
infotext = strings.already_submitted % article.resubmit_link()
|
|
|
|
check_cheating('comments')
|
|
|
|
# figure out number to show based on the menu
|
|
user_num = c.user.pref_num_comments or g.num_comments
|
|
num = g.max_comments if num_comments == 'true' else user_num
|
|
|
|
builder = CommentBuilder(article, CommentSortMenu.operator(sort),
|
|
comment, context)
|
|
listing = NestedListing(builder, num = num,
|
|
parent_name = article._fullname)
|
|
|
|
displayPane = PaneStack()
|
|
|
|
# if permalink page, add that message first to the content
|
|
if comment:
|
|
displayPane.append(PermalinkMessage(article.permalink))
|
|
|
|
# insert reply box only for logged in user
|
|
if c.user_is_loggedin and article.subreddit_slow.can_comment(c.user):
|
|
displayPane.append(CommentReplyBox())
|
|
#no comment box for permalinks
|
|
if not comment:
|
|
displayPane.append(CommentReplyBox(link_name =
|
|
article._fullname))
|
|
# finally add the comment listing
|
|
displayPane.append(listing.listing())
|
|
|
|
loc = None if c.focal_comment or context is not None else 'comments'
|
|
|
|
res = LinkInfoPage(link = article, comment = comment,
|
|
content = displayPane,
|
|
nav_menus = [CommentSortMenu(default = sort),
|
|
NumCommentsMenu(article.num_comments,
|
|
default=num_comments)],
|
|
infotext = infotext).render()
|
|
return res
|
|
|
|
|
|
@base_listing
|
|
@validate(vuser = VExistingUname('username'),
|
|
location = nop('location', default = ''),
|
|
sort = VMenu('location', SortMenu),
|
|
time = VMenu('location', TimeMenu))
|
|
def GET_user(self, num, vuser, sort, time, after, reverse, count, location, **env):
|
|
"""user profile pages"""
|
|
|
|
# the validator will ensure that vuser is a valid account
|
|
if not vuser:
|
|
return self.abort404()
|
|
|
|
# hide spammers profile pages
|
|
if (not c.user_is_loggedin or
|
|
(c.user._id != vuser._id and not c.user_is_admin)) \
|
|
and vuser._spam:
|
|
return self.abort404()
|
|
|
|
check_cheating('user')
|
|
|
|
content_pane = PaneStack()
|
|
|
|
# enable comments displaying with their titles when rendering
|
|
c.profilepage = True
|
|
listing = None
|
|
|
|
db_sort = SortMenu.operator(sort)
|
|
db_time = TimeMenu.operator(time)
|
|
|
|
# function for extracting the proper thing if query is a relation (see liked)
|
|
prewrap_fn = None
|
|
|
|
# default (nonexistent) query to trip an error on if location is unhandles
|
|
query = None
|
|
|
|
# build the sort menus for the space above the content
|
|
sortbar = [SortMenu(default = sort), TimeMenu(default = time)]
|
|
|
|
# overview page is a merge of comments and links
|
|
if location == 'overview':
|
|
self.check_modified(vuser, 'overview')
|
|
links = Link._query(Link.c.author_id == vuser._id,
|
|
Link.c._spam == (True, False))
|
|
comments = Comment._query(Comment.c.author_id == vuser._id,
|
|
Comment.c._spam == (True, False))
|
|
query = thing.Merge((links, comments), sort = db_sort, data = True)
|
|
|
|
elif location == 'comments':
|
|
self.check_modified(vuser, 'commented')
|
|
query = Comment._query(Comment.c.author_id == vuser._id,
|
|
Comment.c._spam == (True, False),
|
|
sort = db_sort)
|
|
|
|
elif location == 'submitted':
|
|
self.check_modified(vuser, 'submitted')
|
|
query = Link._query(Link.c.author_id == vuser._id,
|
|
Link.c._spam == (True, False),
|
|
sort = db_sort)
|
|
|
|
# (dis)liked page: pull votes and extract thing2
|
|
elif ((location == 'liked' or location == 'disliked') and
|
|
votes_visible(vuser)):
|
|
self.check_modified(vuser, location)
|
|
rel = Vote.rel(vuser, Link)
|
|
query = rel._query(rel.c._thing1_id == vuser._id,
|
|
rel.c._t2_deleted == False)
|
|
query._eager(True, True)
|
|
|
|
if location == 'liked':
|
|
query._filter(rel.c._name == '1')
|
|
else:
|
|
query._filter(rel.c._name == '-1')
|
|
sortbar = []
|
|
query._sort = desc('_date')
|
|
prewrap_fn = lambda x: x._thing2
|
|
|
|
# TODO: this should be handled with '' above once merges work
|
|
elif location == 'hidden' and votes_visible(vuser):
|
|
db_time = None
|
|
query = SaveHide._query(SaveHide.c._thing1_id == vuser._id,
|
|
SaveHide.c._name == 'hide',
|
|
eager_load = True,
|
|
thing_data = True)
|
|
sortbar = []
|
|
query._sort = desc('_date')
|
|
prewrap_fn = lambda x: x._thing2
|
|
|
|
# any admin pages live here.
|
|
elif c.user_is_admin:
|
|
db_time = None
|
|
query, prewrap_fn = admin_profile_query(vuser, location, db_sort)
|
|
|
|
if query is None:
|
|
return self.abort404()
|
|
|
|
if db_time:
|
|
query._filter(db_time)
|
|
|
|
|
|
builder = QueryBuilder(query, num = num, prewrap_fn = prewrap_fn,
|
|
after = after, count = count, reverse = reverse,
|
|
wrap = ListingController.builder_wrapper)
|
|
listing = LinkListing(builder)
|
|
|
|
if listing:
|
|
content_pane.append(listing.listing())
|
|
|
|
titles = {'': _("overview for %(user)s on %(site)s"),
|
|
'comments': _("comments by %(user)s on %(site)s"),
|
|
'submitted': _("submitted by %(user)s on %(site)s"),
|
|
'liked': _("liked by %(user)s on %(site)s"),
|
|
'disliked': _("disliked by %(user)s on %(site)s"),
|
|
'hidden': _("hidden by %(user)s on %(site)s")}
|
|
title = titles.get(location, _('profile for %(user)s')) \
|
|
% dict(user = vuser.name, site = c.site.name)
|
|
|
|
return ProfilePage(vuser, title = title,
|
|
nav_menus = sortbar,
|
|
content = content_pane).render()
|
|
|
|
|
|
@validate(VUser(),
|
|
location = nop("location"))
|
|
def GET_prefs(self, location=''):
|
|
"""Preference page"""
|
|
content = None
|
|
infotext = None
|
|
if not location or location == 'options':
|
|
content = PrefOptions(done=request.get.get('done'))
|
|
elif location == 'friends':
|
|
content = PaneStack()
|
|
infotext = strings.friends % Friends.path
|
|
content.append(FriendList())
|
|
elif location == 'update':
|
|
content = PrefUpdate()
|
|
elif location == 'delete':
|
|
content = PrefDelete()
|
|
|
|
return PrefsPage(content = content, infotext=infotext).render()
|
|
|
|
@validate(VUser(),
|
|
name = nop('name'))
|
|
def GET_newreddit(self, name):
|
|
"""Create a reddit form"""
|
|
title = _('create a reddit')
|
|
content=CreateSubreddit(name = name or '')
|
|
res = FormPage(_("create a reddit"),
|
|
content = content,
|
|
).render()
|
|
return res
|
|
|
|
@base_listing
|
|
@validate(location = nop('location'))
|
|
def GET_editreddit(self, location, num, after, reverse, count):
|
|
"""Edit reddit form. """
|
|
if isinstance(c.site, FakeSubreddit):
|
|
return self.abort404()
|
|
|
|
# moderator is either reddit's moderator or an admin
|
|
is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin
|
|
|
|
if is_moderator and location == 'edit':
|
|
pane = CreateSubreddit(site = c.site)
|
|
elif location == 'moderators':
|
|
pane = ModList(editable = is_moderator)
|
|
elif is_moderator and location == 'banned':
|
|
pane = BannedList(editable = is_moderator)
|
|
elif location == 'contributors' and c.site.type != 'public':
|
|
pane = ContributorList(editable = is_moderator)
|
|
elif is_moderator and location == 'spam':
|
|
links = Link._query(Link.c._spam == True)
|
|
comments = Comment._query(Comment.c._spam == True)
|
|
query = thing.Merge((links, comments),
|
|
sort = desc('_date'),
|
|
data = True,
|
|
*c.site.query_rules())
|
|
|
|
builder = QueryBuilder(query, num = num, after = after,
|
|
count = count, reverse = reverse,
|
|
wrap = ListingController.builder_wrapper)
|
|
listing = LinkListing(builder)
|
|
pane = listing.listing()
|
|
else:
|
|
return self.abort404()
|
|
|
|
return EditReddit(content = pane).render()
|
|
|
|
def GET_stats(self):
|
|
"""The stats page."""
|
|
return BoringPage(_("stats"), content = UserStats()).render()
|
|
|
|
# filter for removing punctuation which could be interpreted as lucene syntax
|
|
related_replace_regex = re.compile('[?\\&|!{}+~^()":*-]+')
|
|
related_replace_with = ' '
|
|
|
|
@base_listing
|
|
@validate(article = VLink('article'))
|
|
def GET_related(self, num, article, after, reverse, count):
|
|
"""Related page: performs a search using title of article as
|
|
the search query."""
|
|
title = c.site.name + ((': ' + article.title) if hasattr(article, 'title') else '')
|
|
|
|
query = self.related_replace_regex.sub(self.related_replace_with,
|
|
article.title)
|
|
if len(query) > 1024:
|
|
# could get fancier and break this into words, but titles
|
|
# longer than this are typically ascii art anyway
|
|
query = query[0:1023]
|
|
|
|
num, t, pane = self._search(query, time = 'all',
|
|
count = count,
|
|
after = after, reverse = reverse, num = num,
|
|
ignore = [article._fullname],
|
|
types = [Link])
|
|
res = LinkInfoPage(link = article, content = pane).render()
|
|
return res
|
|
|
|
@base_listing
|
|
@validate(query = nop('q'))
|
|
def GET_search_reddits(self, query, reverse, after, count, num):
|
|
"""Search reddits by title and description."""
|
|
num, t, spane = self._search(query, num = num, types = [Subreddit],
|
|
sort='points desc', time='all',
|
|
after = after, reverse = reverse,
|
|
count = count)
|
|
|
|
res = SubredditsPage(content=spane,
|
|
prev_search = query,
|
|
elapsed_time = t,
|
|
num_results = num,
|
|
title = _("search results")).render()
|
|
return res
|
|
|
|
verify_langs_regex = re.compile(r"^[a-z][a-z](,[a-z][a-z])*$")
|
|
@base_listing
|
|
@validate(query=nop('q'),
|
|
time = VMenu('action', TimeMenu, remember = False),
|
|
langs = nop('langs'))
|
|
def GET_search(self, query, num, time, reverse, after, count, langs):
|
|
"""Search links page."""
|
|
if query and '.' in query:
|
|
url = sanitize_url(query, require_scheme = True)
|
|
if url:
|
|
return self.redirect("/submit" + query_string({'url':url}))
|
|
|
|
if langs and self.verify_langs_regex.match(langs):
|
|
langs = langs.split(',')
|
|
else:
|
|
langs = None
|
|
|
|
num, t, spane = self._search(query, time=time,
|
|
num = num, after = after,
|
|
reverse = reverse,
|
|
count = count, types = [Link])
|
|
|
|
if not isinstance(c.site,FakeSubreddit):
|
|
my_reddits_link = "/search%s" % query_string({'q': query})
|
|
all_reddits_link = "%s/search%s" % (subreddit.All.path,
|
|
query_string({'q': query}))
|
|
infotext = strings.searching_a_reddit % {'reddit_name': c.site.name,
|
|
'reddit_link': c.site.path,
|
|
'my_reddits_link': my_reddits_link,
|
|
'all_reddits_link': all_reddits_link}
|
|
else:
|
|
infotext = None
|
|
|
|
res = SearchPage(_('search results'), query, t, num, content=spane,
|
|
nav_menus = [TimeMenu(default = time)],
|
|
infotext = infotext).render()
|
|
|
|
return res
|
|
|
|
def _search(self, query = '', time=None,
|
|
sort = 'hot desc',
|
|
after = None, reverse = False, num = 25,
|
|
ignore = None, count=0, types = None,
|
|
langs = None):
|
|
"""Helper function for interfacing with search. Basically a
|
|
thin wrapper for SearchBuilder."""
|
|
builder = SearchBuilder(query, num = num,
|
|
sort = sort,
|
|
after = after, reverse = reverse,
|
|
count = count, types = types,
|
|
time = time, ignore = ignore,
|
|
langs = langs,
|
|
wrap = ListingController.builder_wrapper)
|
|
listing = LinkListing(builder, show_nums=True)
|
|
|
|
# have to do it in two steps since total_num and timing are only
|
|
# computed after fetch_more
|
|
res = listing.listing()
|
|
return builder.total_num, builder.timing, res
|
|
|
|
|
|
|
|
def GET_login(self):
|
|
"""The /login form. No link to this page exists any more on
|
|
the site (all actions invoking it now go through the login
|
|
cover). However, this page is still used for logging the user
|
|
in during submission or voting from the bookmarklets."""
|
|
|
|
# dest is the location to redirect to upon completion
|
|
dest = request.get.get('dest','') or request.referer or '/'
|
|
return LoginPage(dest = dest).render()
|
|
|
|
def GET_logout(self):
|
|
"""wipe login cookie and redirect to referer."""
|
|
self.logout()
|
|
dest = request.referer or '/'
|
|
return self.redirect(dest)
|
|
|
|
|
|
@validate(VUser())
|
|
def GET_adminon(self):
|
|
"""Enable admin interaction with site"""
|
|
#check like this because c.user_is_admin is still false
|
|
if not c.user.name in g.admins:
|
|
return self.abort404()
|
|
self.login(c.user, admin = True)
|
|
|
|
dest = request.referer or '/'
|
|
return self.redirect(dest)
|
|
|
|
@validate(VAdmin())
|
|
def GET_adminoff(self):
|
|
"""disable admin interaction with site."""
|
|
if not c.user.name in g.admins:
|
|
return self.abort404()
|
|
self.login(c.user, admin = False)
|
|
|
|
dest = request.referer or '/'
|
|
return self.redirect(dest)
|
|
|
|
def GET_validuser(self):
|
|
"""checks login cookie to verify that a user is logged in and
|
|
returns their user name"""
|
|
c.response_content_type = 'text/plain'
|
|
if c.user_is_loggedin:
|
|
return c.user.name
|
|
else:
|
|
return ''
|
|
|
|
|
|
@validate(VUser(),
|
|
VSRSubmitPage(),
|
|
url = VRequired('url', None),
|
|
title = VRequired('title', None))
|
|
def GET_submit(self, url, title):
|
|
"""Submit form."""
|
|
if url and not request.get.get('resubmit'):
|
|
# check to see if the url has already been submitted
|
|
listing = link_listing_by_url(url)
|
|
redirect_link = None
|
|
if listing.things:
|
|
# if there is only one submission, the operation is clear
|
|
if len(listing.things) == 1:
|
|
redirect_link = listing.things[0]
|
|
# if there is more than one, check the users' subscriptions
|
|
else:
|
|
subscribed = [l for l in listing.things
|
|
if c.user_is_loggedin
|
|
and l.subreddit.is_subscriber_defaults(c.user)]
|
|
|
|
#if there is only 1 link to be displayed, just go there
|
|
if len(subscribed) == 1:
|
|
redirect_link = subscribed[0]
|
|
else:
|
|
infotext = strings.multiple_submitted % \
|
|
listing.things[0].resubmit_link()
|
|
res = BoringPage(_("seen it"),
|
|
content = listing,
|
|
infotext = infotext).render()
|
|
return res
|
|
|
|
# we've found a link already. Redirect to its permalink page
|
|
if redirect_link:
|
|
return self.redirect(redirect_link.already_submitted_link)
|
|
|
|
captcha = Captcha() if c.user.needs_captcha() else None
|
|
sr_names = Subreddit.submit_sr_names(c.user) if c.default_sr else ()
|
|
|
|
return FormPage(_("submit"),
|
|
content=NewLink(url=url or '',
|
|
title=title or '',
|
|
subreddits = sr_names,
|
|
captcha=captcha)).render()
|
|
|
|
@validate(msg_hash = nop('x'))
|
|
def GET_optout(self, msg_hash):
|
|
email, sent = opt_out(msg_hash)
|
|
if not email:
|
|
return self.abort404()
|
|
return BoringPage(_("opt out"),
|
|
content = OptOut(email = email, leave = True,
|
|
sent = sent,
|
|
msg_hash = msg_hash)).render()
|
|
|
|
|
|
@validate(msg_hash = nop('x'))
|
|
def GET_optin(self, msg_hash):
|
|
email, sent = opt_in(msg_hash)
|
|
if not email:
|
|
return self.abort404()
|
|
return BoringPage(_("welcome back"),
|
|
content = OptOut(email = email, leave = False,
|
|
sent = sent,
|
|
msg_hash = msg_hash)).render()
|
|
|