Files
reddit/r2/r2/controllers/api.py

1348 lines
52 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 reddit_base import RedditController
from pylons.i18n import _
from pylons import c, request
from validator import *
from r2.models import *
from r2.models.subreddit import Default as DefaultSR
import r2.models.thing_changes as tc
from r2.controllers import ListingController
from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified
from r2.lib.utils import query_string, to36, timefromnow
from r2.lib.wrapped import Wrapped
from r2.lib.pages import FriendList, ContributorList, ModList, \
BannedList, BoringPage, FormPage, NewLink, CssError, UploadedImage
from r2.lib.menus import CommentSortMenu
from r2.lib.normalized_hot import expire_hot
from r2.lib.captcha import get_iden
from r2.lib import emailer
from r2.lib.strings import strings
from r2.lib.memoize import clear_memo
from r2.lib.filters import _force_unicode, websafe_json
from r2.lib.db import queries
from r2.lib import cssfilter
from r2.lib import tracking
from r2.lib.media import force_thumbnail, thumbnail_url
from r2.lib.comment_tree import add_comment, delete_comment
from simplejson import dumps
from datetime import datetime, timedelta
from md5 import md5
from r2.lib.promote import promote, unpromote, get_promoted
def link_listing_by_url(url, count = None):
"""
Generates a listing of links which share a common url, filtered by
the subreddit the current user is subscribed to (or the default
list of the user is not logged in.)
"""
try:
links = list(tup(Link._by_url(url, sr = c.site)))
links.sort(key = lambda x: -x._score)
if count is not None:
links = links[:count]
except NotFound:
links = ()
user = c.user if c.user_is_loggedin else None
names = [l._fullname for l in links
if l.sr_id in Subreddit.user_subreddits(user, limit = None)]
builder = IDBuilder(names, num = 25)
return LinkListing(builder).listing()
class ApiController(RedditController):
"""
Controller which deals with almost all AJAX site interaction.
"""
def response_func(self, **kw):
data = dumps(kw)
if request.method == "GET" and request.GET.get("callback"):
return "%s(%s)" % (websafe_json(request.GET.get("callback")),
websafe_json(data))
return self.sendstring(data)
@validatedForm()
def ajax_login_redirect(self, form, jquery, dest):
jquery.redirect("/login" + query_string(dict(dest=dest)))
@validate(link = VUrl(['url']),
count = VLimit('limit'))
def GET_info(self, link, count):
"""
Get's a listing of links which have the provided url.
"""
listing = None
if link and errors.ALREADY_SUB in c.errors:
listing = link_listing_by_url(request.params.get('url'),
count = count)
return BoringPage(_("API"), content = listing).render()
@validatedForm(VCaptcha(),
name=VRequired('name', errors.NO_NAME),
email=VRequired('email', errors.NO_EMAIL),
replyto = ValidEmails("replyto", num = 1),
reason = VOneOf('reason', ('ad_inq', 'feedback')),
message=VRequired('message', errors.NO_MESSAGE),
)
def POST_feedback(self, form, jquery, name, email,
replyto, reason, message):
if not (form.has_errors('name', errors.NO_NAME) or
form.has_errors('email', errors.NO_EMAIL) or
(request.POST.get("replyto") and replyto is None and
form.has_errors("replyto", errors.BAD_EMAILS)) or
form.has_errors('personal', errors.NO_MESSAGE) or
form.chk_captcha(errors.BAD_CAPTCHA)):
if reason != 'ad_inq':
emailer.feedback_email(email, message, name = name or '',
reply_to = replyto or '')
else:
emailer.ad_inq_email(email, message, name = name or '',
reply_to = replyto or '')
form.set_html(".success", _("thanks for your message! "
"you should hear back from us shortly."))
form.set_inputs(personal = "", captcha = "")
POST_ad_inq = POST_feedback
@validatedForm(VCaptcha(),
VUser(),
VModhash(),
ip = ValidIP(),
to = VExistingUname('to'),
subject = VRequired('subject', errors.NO_SUBJECT),
body = VMessage('message'))
def POST_compose(self, form, jquery, to, subject, body, ip):
"""
handles message composition under /message/compose.
"""
if not (form.has_errors("to", errors.USER_DOESNT_EXIST,
errors.NO_USER) or
form.has_errors("subject", errors.NO_SUBJECT) or
form.has_errors("message", errors.NO_MSG_BODY) or
form.chk_captcha(errors.BAD_CAPTCHA)):
spam = (c.user._spam or
errors.BANNED_IP in c.errors or
errors.BANNED_DOMAIN in c.errors)
m, inbox_rel = Message._new(c.user, to, subject, body, ip, spam)
form.set_html(".success", _("your message has been delivered"))
form.set_inputs(to = "", subject = "", message = "")
if g.write_query_queue:
queries.new_message(m, inbox_rel)
@validatedForm(VUser(),
VCaptcha(),
ValidDomain('url'),
VRatelimit(rate_user = True, rate_ip = True,
prefix = "rate_submit_"),
ip = ValidIP(),
sr = VSubmitSR('sr'),
url = VUrl(['url', 'sr']),
title = VTitle('title'),
save = VBoolean('save'),
)
def POST_submit(self, form, jquery, url, title, save, sr, ip):
if isinstance(url, (unicode, str)):
form.set_inputs(url = url)
should_ratelimit = sr.should_ratelimit(c.user, 'link')
#remove the ratelimit error if the user's karma is high
if not should_ratelimit:
c.errors.remove(errors.RATELIMIT)
# check for no url, or clear that error field on return
if form.has_errors("url", errors.NO_URL, errors.BAD_URL):
pass
elif form.has_errors("url", errors.ALREADY_SUB):
jquery.redirect(url[0].already_submitted_link)
# check for title, otherwise look it up and return it
elif form.has_errors("title", errors.NO_TITLE):
# try to fetch the title
title = get_title(url)
if title:
# note: focus first, since it clears the form
form.set_inputs(title = title)
# wipe out the no title error
form.clear_errors(errors.NO_TITLE)
return
elif (form.has_errors("title", errors.TITLE_TOO_LONG) or
form.chk_captcha(errors.BAD_CAPTCHA, errors.RATELIMIT)):
pass
if form.has_error() or not title: return
# check whether this is spam:
spam = (c.user._spam or
errors.BANNED_IP in c.errors or
errors.BANNED_DOMAIN in c.errors)
# well, nothing left to do but submit it
l = Link._submit(request.post.title, url, c.user, sr, ip, spam)
if url.lower() == 'self':
l.url = l.make_permalink_slow()
l.is_self = True
l._commit()
l.set_url_cache()
v = Vote.vote(c.user, l, True, ip, spam)
if save:
r = l._save(c.user)
if g.write_query_queue:
queries.new_savehide(r)
#reset the hot page
if v.valid_thing:
expire_hot(sr)
#set the ratelimiter
if should_ratelimit:
VRatelimit.ratelimit(rate_user=True, rate_ip = True,
prefix = "rate_submit_")
#update the queries
if g.write_query_queue:
queries.new_link(l)
queries.new_vote(v)
#update the modified flags
set_last_modified(c.user, 'overview')
set_last_modified(c.user, 'submitted')
# flag search indexer that something has changed
tc.changed(l)
# make_permalink is designed for links that can be set to _top
# here, we need to generate an ajax redirect as if we were not on a
# cname.
cname = c.cname
c.cname = False
path = l.make_permalink_slow()
c.cname = cname
jquery.redirect(path)
def _login(self, jquery, user, dest='', rem = None):
"""
AJAX login handler, used by both login and register to set the
user cookie and send back a redirect.
"""
self.login(user, rem = rem)
dest = dest or request.referer or '/'
jquery.redirect(dest)
@validatedForm(user = VLogin(['user', 'passwd']),
dest = nop('dest'),
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_login(self, form, jquery, user, dest, rem, reason):
if reason and reason[0] == 'redirect':
dest = reason[1]
if not form.has_errors("passwd", errors.WRONG_PASSWORD):
self._login(jquery, user, dest, rem)
@validatedForm(VCaptcha(),
VRatelimit(rate_ip = True, prefix = "rate_register_"),
name = VUname(['user']),
email = ValidEmails("email", num = 1),
password = VPassword(['passwd', 'passwd2']),
dest = nop('dest'),
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_register(self, form, jquery, name, email,
password, dest, rem, reason):
if not (form.has_errors("user", errors.BAD_USERNAME,
errors.USERNAME_TAKEN) or
form.has_errors("email", errors.BAD_EMAILS) or
form.has_errors("passwd", errors.BAD_PASSWORD) or
form.has_errors("passwd2", errors.BAD_PASSWORD_MATCH) or
form.chk_captcha(errors.BAD_CAPTCHA,errors.RATELIMIT)):
user = register(name, password)
VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")
#anything else we know (email, languages)?
if email:
user.email = email
user.pref_lang = c.lang
if c.content_langs == 'all':
user.pref_content_langs = 'all'
else:
langs = list(c.content_langs)
langs.sort()
user.pref_content_langs = tuple(langs)
d = c.user._dirties.copy()
user._commit()
c.user = user
if reason:
if reason[0] == 'redirect':
dest = reason[1]
elif reason[0] == 'subscribe':
for sr, sub in reason[1].iteritems():
self._subscribe(sr, sub)
self._login(jquery, user, dest, rem)
@noresponse(VUser(),
VModhash(),
container = VByName('id'))
def POST_leave_moderator(self, container):
"""
Handles self-removal as moderator from a subreddit as rendered
in the subreddit sidebox on any of that subreddit's pages.
"""
if container and container.is_moderator(c.user):
container.remove_moderator(c.user)
@noresponse(VUser(),
VModhash(),
container = VByName('id'))
def POST_leave_contributor(self, container):
"""
same comment as for POST_leave_moderator.
"""
if container and container.is_contributor(c.user):
container.remove_contributor(c.user)
@noresponse(VUser(),
VModhash(),
nuser = VExistingUname('name'),
iuser = VByName('id'),
container = VByName('container'),
type = VOneOf('type', ('friend', 'moderator',
'contributor', 'banned')))
def POST_unfriend(self, nuser, iuser, container, type):
"""
Handles removal of a friend (a user-user relation) or removal
of a user's priviledges from a subreddit (a user-subreddit
relation). The user can either be passed in by name (nuser)
or buy fullname (iuser). 'container' will either be the
current user or the subreddit.
"""
# The user who made the request must be an admin or a moderator
# for the privilege change to succeed.
if (not c.user_is_admin
and (type in ('moderator','contributer','banned')
and not c.site.is_moderator(c.user))):
abort(403, 'forbidden')
# if we are (strictly) unfriending, the container had better
# be the current user.
if type == "friend" and container != c.user:
abort(403, 'forbidden')
fn = getattr(container, 'remove_' + type)
fn(iuser or nuser)
@validatedForm(VUser(),
VModhash(),
ip = ValidIP(),
friend = VExistingUname('name'),
container = VByName('container'),
type = VOneOf('type', ('friend', 'moderator',
'contributor', 'banned')))
def POST_friend(self, form, jquery, ip, friend,
container, type):
"""
Complement to POST_unfriend: handles friending as well as
privilege changes on subreddits.
"""
fn = getattr(container, 'add_' + type)
# The user who made the request must be an admin or a moderator
# for the privilege change to succeed.
if (not c.user_is_admin
and (type in ('moderator','contributer','banned')
and not c.site.is_moderator(c.user))):
abort(403,'forbidden')
# if we are (strictly) friending, the container had better
# be the current user.
if type == "friend" and container != c.user:
abort(403,'forbidden')
elif not form.has_errors("name",
errors.USER_DOESNT_EXIST, errors.NO_USER):
new = fn(friend)
cls = dict(friend=FriendList,
moderator=ModList,
contributor=ContributorList,
banned=BannedList).get(type)
form.set_inputs(name = "")
form.set_html(".status:first", _("added"))
if new and cls:
user_row = cls().user_row(friend)
jquery("table").insert_table_rows(user_row)
if type != 'friend':
msg = strings.msg_add_friend.get(type)
subj = strings.subj_add_friend.get(type)
if msg and subj and friend.name != c.user.name:
# fullpath with domain needed or the markdown link
# will break
d = dict(url = container.path,
title = container.title)
msg = msg % d
subj = subj % d
Message._new(c.user, friend, subj, msg, ip,
c.user._spam)
@validatedForm(VUser('curpass', default = ''),
VModhash(),
email = ValidEmails("email", num = 1),
password = VPassword(['newpass', 'verpass']))
def POST_update(self, form, jquery, email, password):
"""
handles /prefs/update for updating email address and password.
"""
# password is required to proceed
if form.has_errors("curpass", errors.WRONG_PASSWORD):
form.set_input(curpass = "")
return
# check if the email is valid. If one is given and it is
# different from the current address (or there is not one
# currently) apply it
updated = False
if (not form.has_errors("email", errors.BAD_EMAILS) and
email and (not hasattr(c.user,'email') or c.user.email != email)):
c.user.email = email
c.user._commit()
form.set_html('.status', _('your email has been updated'))
updated = True
# change password
if (password and
not (form.has_errors("newpass", errors.BAD_PASSWORD) or
form.has_errors("verpass", errors.BAD_PASSWORD_MATCH))):
change_password(c.user, password)
if updated:
form.set_html(".status",
_('your email and password have been updated'))
else:
form.set_html('.status',
_('your password has been updated'))
# the password has changed, so the user's cookie has been
# invalidated. drop a new cookie.
self.login(c.user)
@validatedForm(VUser(),
VModhash(),
areyousure1 = VOneOf('areyousure1', ('yes', 'no')),
areyousure2 = VOneOf('areyousure2', ('yes', 'no')),
areyousure3 = VOneOf('areyousure3', ('yes', 'no')))
def POST_delete_user(self, form, jquery,
areyousure1, areyousure2, areyousure3):
"""
/prefs/delete. Make sure there are three yes's.
"""
if areyousure1 == areyousure2 == areyousure3 == 'yes':
c.user.delete()
jquery.redirect('/?deleted=true')
else:
form.set_html('.status', _("see? you don't really want to leave"))
@noresponse(VUser(),
VModhash(),
thing = VByNameIfAuthor('id'))
def POST_del(self, thing):
'''for deleting all sorts of things'''
thing._deleted = True
thing._commit()
# flag search indexer that something has changed
tc.changed(thing)
#expire the item from the sr cache
if isinstance(thing, Link):
sr = thing.subreddit_slow
expire_hot(sr)
if g.use_query_cache:
queries.new_link(thing)
#comments have special delete tasks
elif isinstance(thing, Comment):
thing._delete()
delete_comment(thing)
if g.use_query_cache:
queries.new_comment(thing, None)
@noresponse(VUser(), VModhash(),
thing = VByName('id'))
def POST_report(self, thing):
'''for reporting...'''
if (thing and not thing._deleted and
not (hasattr(thing, "promoted") and thing.promoted)):
Report.new(c.user, thing)
@validatedForm(VUser(), VModhash(),
comment = VByNameIfAuthor('parent'),
body = VComment('comment'))
def POST_editcomment(self, form, jquery, comment, body):
if not form.has_errors("comment",
errors.BAD_COMMENT, errors.COMMENT_TOO_LONG,
errors.NOT_AUTHOR):
comment.body = body
if not c.user_is_admin: comment.editted = True
comment._commit()
jquery.replace_things(comment, True, True)
# flag search indexer that something has changed
tc.changed(comment)
@validatedForm(VUser(),
VModhash(),
VRatelimit(rate_user = True, rate_ip = True,
prefix = "rate_comment_"),
ip = ValidIP(),
parent = VSubmitParent('parent'),
comment = VComment('comment'))
def POST_comment(self, commentform, jquery, parent, comment, ip):
should_ratelimit = True
#check the parent type here cause we need that for the
#ratelimit checks
if isinstance(parent, Message):
is_message = True
should_ratelimit = False
else:
is_message = False
is_comment = True
if isinstance(parent, Link):
link = parent
parent_comment = None
else:
link = Link._byID(parent.link_id, data = True)
parent_comment = parent
sr = parent.subreddit_slow
if not sr.should_ratelimit(c.user, 'comment'):
should_ratelimit = False
#remove the ratelimit error if the user's karma is high
if not should_ratelimit:
c.errors.remove(errors.RATELIMIT)
if (not commentform.has_errors("comment",
errors.BAD_COMMENT,
errors.COMMENT_TOO_LONG,
errors.RATELIMIT) and
not commentform.has_errors("parent",
errors.DELETED_COMMENT)):
spam = (c.user._spam or errors.BANNED_IP in c.errors)
if is_message:
to = Account._byID(parent.author_id)
subject = parent.subject
re = "re: "
if not subject.startswith(re):
subject = re + subject
item, inbox_rel = Message._new(c.user, to, subject,
comment, ip, spam)
item.parent_id = parent._id
else:
item, inbox_rel = Comment._new(c.user, link, parent_comment,
comment, ip, spam)
Vote.vote(c.user, item, True, ip)
# flag search indexer that something has changed
tc.changed(item)
#update last modified
set_last_modified(c.user, 'overview')
set_last_modified(c.user, 'commented')
set_last_modified(link, 'comments')
#update the comment cache
add_comment(item)
# clean up the submission form and remove it from the DOM (if reply)
t = commentform.find("textarea")
t.attr('rows', 3).html("").attr("value", "")
if isinstance(parent, (Comment, Message)):
commentform.remove()
jquery.things(parent._fullname).set_html(".reply-button:first",
_("replied"))
# insert the new comment
jquery.insert_things(item)
# remove any null listings that may be present
jquery("#noresults").hide()
#update the queries
if g.write_query_queue:
if is_message:
queries.new_message(item, inbox_rel)
else:
queries.new_comment(item, inbox_rel)
#set the ratelimiter
if should_ratelimit:
VRatelimit.ratelimit(rate_user=True, rate_ip = True,
prefix = "rate_comment_")
@validatedForm(VUser(),
VModhash(),
VCaptcha(),
VRatelimit(rate_user = True, rate_ip = True,
prefix = "rate_share_"),
share_from = VLength('share_from', length = 100),
emails = ValidEmails("share_to"),
reply_to = ValidEmails("replyto", num = 1),
message = VLength("message", length = 1000),
thing = VByName('parent'))
def POST_share(self, shareform, jquery, emails, thing, share_from, reply_to,
message):
# remove the ratelimit error if the user's karma is high
sr = thing.subreddit_slow
should_ratelimit = sr.should_ratelimit(c.user, 'link')
if not should_ratelimit:
c.errors.remove(errors.RATELIMIT)
if emails and (errors.NO_EMAILS in c.errors):
c.errors.remove(errors.NO_EMAILS)
# share_from and messages share a comment_too_long error.
# finding an error on one necessitates hiding the other error
if shareform.has_errors("share_from", errors.COMMENT_TOO_LONG):
shareform.find(".message-errors").children().hide()
elif shareform.has_errors("message", errors.COMMENT_TOO_LONG):
shareform.find(".share-form-errors").children().hide()
# reply_to and share_to also share errors...
elif shareform.has_errors("share_to", errors.BAD_EMAILS,
errors.NO_EMAILS,
errors.TOO_MANY_EMAILS):
shareform.find(".reply-to-errors").children().hide()
elif shareform.has_errors("replyto", errors.BAD_EMAILS,
errors.NO_EMAILS,
errors.TOO_MANY_EMAILS):
shareform.find(".share-to-errors").children().hide()
# lastly, check the captcha.
elif shareform.chk_captcha(errors.BAD_CAPTCHA, errors.RATELIMIT):
pass
else:
c.user.add_share_emails(emails)
c.user._commit()
link = jquery.things(thing._fullname)
link.set_html(".share", _("shared"))
shareform.html("<div class='clearleft'></div>"
"<p class='error'>%s</p>" %
_("your link has been shared."))
emailer.share(thing, emails, from_name = share_from or "",
body = message or "", reply_to = reply_to or "")
#set the ratelimiter
if should_ratelimit:
VRatelimit.ratelimit(rate_user=True, rate_ip = True,
prefix = "rate_share_")
@noresponse(VUser(),
VModhash(),
vote_type = VVotehash(('vh', 'id')),
ip = ValidIP(),
dir = VInt('dir', min=-1, max=1),
thing = VByName('id'))
def POST_vote(self, dir, thing, ip, vote_type):
ip = request.ip
user = c.user
spam = (c.user._spam or
errors.BANNED_IP in c.errors or
errors.CHEATER in c.errors)
if thing:
dir = (True if dir > 0
else False if dir < 0
else None)
organic = vote_type == 'organic'
v = Vote.vote(user, thing, dir, ip, spam, organic)
#update relevant caches
if isinstance(thing, Link):
sr = thing.subreddit_slow
set_last_modified(c.user, 'liked')
set_last_modified(c.user, 'disliked')
if v.valid_thing:
expire_hot(sr)
if g.write_query_queue:
queries.new_vote(v)
# flag search indexer that something has changed
tc.changed(thing)
@validatedForm(VUser(),
VModhash(),
# nop is safe: handled after auth checks below
stylesheet_contents = nop('stylesheet_contents'),
op = VOneOf('op',['save','preview']))
def POST_subreddit_stylesheet(self, form, jquery,
stylesheet_contents = '', op='save'):
if not c.site.can_change_stylesheet(c.user):
return self.abort(403,'forbidden')
if g.css_killswitch:
return self.abort(403,'forbidden')
# validation is expensive. Validate after we've confirmed
# that the changes will be allowed
parsed, report = cssfilter.validate_css(stylesheet_contents)
if report.errors:
error_items = [ CssError(x).render(style='html')
for x in sorted(report.errors) ]
form.set_html(".status", _('validation errors'))
form.set_html(".errors ul", ''.join(error_items))
form.find('.errors').show()
else:
form.find('.errors').hide()
form.set_html(".errors ul", '')
stylesheet_contents_parsed = parsed.cssText if parsed else ''
# if the css parsed, we're going to apply it (both preview & save)
if not report.errors:
jquery.apply_stylesheet(stylesheet_contents_parsed)
if not report.errors and op == 'save':
c.site.stylesheet_contents = stylesheet_contents_parsed
c.site.stylesheet_contents_user = stylesheet_contents
c.site.stylesheet_hash = md5(stylesheet_contents_parsed).hexdigest()
set_last_modified(c.site,'stylesheet_contents')
tc.changed(c.site)
c.site._commit()
form.set_html(".status", _('saved'))
form.set_html(".errors ul", "")
elif op == 'preview':
# try to find a link to use, otherwise give up and
# return
links = cssfilter.find_preview_links(c.site)
if links:
jquery('#preview-table').show()
# do a regular link
jquery('#preview_link_normal').html(
cssfilter.rendered_link(links, media = 'off',
compress=False))
# now do one with media
jquery('#preview_link_media').html(
cssfilter.rendered_link(links, media = 'on',
compress=False))
# do a compressed link
jquery('#preview_link_compressed').html(
cssfilter.rendered_link(links, media = 'off',
compress=True))
# and do a comment
comments = cssfilter.find_preview_comments(c.site)
if comments:
jquery('#preview_comment').html(
cssfilter.rendered_comment(comments))
@validatedForm(VSrModerator(),
VModhash(),
name = VCssName('img_name'))
def POST_delete_sr_img(self, form, jquery, name):
"""
Called called upon requested delete on /about/stylesheet.
Updates the site's image list, and causes the <li> which wraps
the image to be hidden.
"""
# just in case we need to kill this feature from XSS
if g.css_killswitch:
return self.abort(403,'forbidden')
c.site.del_image(name)
c.site._commit()
@validatedForm(VSrModerator(),
VModhash())
def POST_delete_sr_header(self, form, jquery):
"""
Called when the user request that the header on a sr be reset.
"""
# just in case we need to kill this feature from XSS
if g.css_killswitch:
return self.abort(403,'forbidden')
if c.site.header:
c.site.header = None
c.site._commit()
# reset the header image on the page
form.find('#header-img').attr("src", DefaultSR.header)
# hide the button which started this
form.find('#delete-img').hide()
# hide the preview box
form.find('#img-preview-container').hide()
# reset the status boxes
form.set_html('.img-status', _("deleted"))
def GET_upload_sr_img(self, *a, **kw):
"""
Completely unnecessary method which exists because safari can
be dumb too. On page reload after an image has been posted in
safari, the iframe to which the request posted preserves the
URL of the POST, and safari attempts to execute a GET against
it. The iframe is hidden, so what it returns is completely
irrelevant.
"""
return "nothing to see here."
@validate(VSrModerator(),
VModhash(),
file = VLength('file', length=1024*500),
name = VCssName("name"),
header = nop('header'))
def POST_upload_sr_img(self, file, header, name):
"""
Called on /about/stylesheet when an image needs to be replaced
or uploaded, as well as on /about/edit for updating the
header. Unlike every other POST in this controller, this
method does not get called with Ajax but rather is from the
original form POSTing to a hidden iFrame. Unfortunately, this
means the response needs to generate an page with a script tag
to fire the requisite updates to the parent document, and,
more importantly, that we can't use our normal toolkit for
passing those responses back.
The result of this function is a rendered UploadedImage()
object in charge of firing the completedUploadImage() call in
JS.
"""
# default error list (default values will reset the errors in
# the response if no error is raised)
errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "")
try:
cleaned = cssfilter.clean_image(file,'PNG')
if header:
num = None # there is one and only header, and it is unnumbered
elif not name:
# error if the name wasn't specified or didn't satisfy
# the validator
errors['BAD_CSS_NAME'] = _("bad image name")
else:
num = c.site.add_image(name, max_num = g.max_sr_images)
c.site._commit()
except cssfilter.BadImage:
# if the image doesn't clean up nicely, abort
errors["IMAGE_ERROR"] = _("bad image")
except ValueError:
# the add_image method will raise only on too many images
errors['IMAGE_ERROR'] = (
_("too many images (you only get %d)") % g.max_sr_images)
if any(errors.values()):
return UploadedImage("", "", "", errors = errors).render()
else:
# with the image num, save the image an upload to s3. the
# header image will be of the form "${c.site._fullname}.png"
# while any other image will be ${c.site._fullname}_${num}.png
new_url = cssfilter.save_sr_image(c.site, cleaned, num = num)
if header:
c.site.header = new_url
c.site._commit()
return UploadedImage(_('saved'), new_url, name,
errors = errors).render()
@validatedForm(VUser(),
VModhash(),
VRatelimit(rate_user = True,
rate_ip = True,
prefix = 'create_reddit_'),
sr = VByName('sr'),
name = VSubredditName("name"),
title = VSubredditTitle("title"),
domain = VCnameDomain("domain"),
description = VSubredditDesc("description"),
lang = VLang("lang"),
over_18 = VBoolean('over_18'),
show_media = VBoolean('show_media'),
type = VOneOf('type', ('public', 'private', 'restricted'))
)
def POST_site_admin(self, form, jquery, name ='', sr = None, **kw):
# the status button is outside the form -- have to reset by hand
form.parent().set_html('.status', "")
redir = False
kw = dict((k, v) for k, v in kw.iteritems()
if k in ('name', 'title', 'domain', 'description', 'over_18',
'show_media', 'type', 'lang',))
#if a user is banned, return rate-limit errors
if c.user._spam:
time = timeuntil(datetime.now(g.tz) + timedelta(seconds=600))
c.errors.add(errors.RATELIMIT, {'time': time})
domain = kw['domain']
cname_sr = domain and Subreddit._by_domain(domain)
if cname_sr and (not sr or sr != cname_sr):
c.errors.add(errors.USED_CNAME)
if not sr and form.has_errors(None, errors.RATELIMIT):
pass
elif not sr and form.has_errors("name", errors.SUBREDDIT_EXISTS,
errors.BAD_SR_NAME):
form.find('#example_name').hide()
elif form.has_errors('title', errors.NO_TITLE, errors.TITLE_TOO_LONG):
form.find('#example_title').hide()
elif form.has_errors('domain', errors.BAD_CNAME, errors.USED_CNAME):
form.find('#example_domain').hide()
elif (form.has_errors(None, errors.INVALID_OPTION) or
form.has_errors('description', errors.DESC_TOO_LONG)):
pass
#creating a new reddit
elif not sr:
#sending kw is ok because it was sanitized above
sr = Subreddit._new(name = name, **kw)
Subreddit.subscribe_defaults(c.user)
# make sure this user is on the admin list of that site!
if sr.add_subscriber(c.user):
sr._incr('_ups', 1)
sr.add_moderator(c.user)
sr.add_contributor(c.user)
redir = sr.path + "about/edit/"
if not c.user_is_admin:
VRatelimit.ratelimit(rate_user=True,
rate_ip = True,
prefix = "create_reddit_")
#editting an existing reddit
elif sr.is_moderator(c.user) or c.user_is_admin:
#assume sr existed, or was just built
clear_memo('subreddit._by_domain',
Subreddit, _force_unicode(sr.domain))
for k, v in kw.iteritems():
setattr(sr, k, v)
sr._commit()
clear_memo('subreddit._by_domain',
Subreddit, _force_unicode(sr.domain))
# flag search indexer that something has changed
tc.changed(sr)
form.parent().set_html('.status', _("saved"))
if redir:
jquery.redirect(redir)
@noresponse(VModhash(),
VSrCanBan('id'),
thing = VByName('id'))
def POST_ban(self, thing):
thing.moderator_banned = not c.user_is_admin
thing.banner = c.user.name
thing._commit()
# NB: change table updated by reporting
unreport(thing, correct=True, auto=False)
@noresponse(VModhash(),
VSrCanBan('id'),
thing = VByName('id'))
def POST_unban(self, thing):
# NB: change table updated by reporting
unreport(thing, correct=False)
@noresponse(VModhash(),
VSrCanBan('id'),
thing = VByName('id'))
def POST_ignore(self, thing):
# NB: change table updated by reporting
unreport(thing, correct=False)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_save(self, thing):
r = thing._save(c.user)
if g.write_query_queue:
queries.new_savehide(r)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_unsave(self, thing):
r = thing._unsave(c.user)
if g.write_query_queue and r:
queries.new_savehide(r)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_hide(self, thing):
r = thing._hide(c.user)
if g.write_query_queue:
queries.new_savehide(r)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_unhide(self, thing):
r = thing._unhide(c.user)
if g.write_query_queue and r:
queries.new_savehide(r)
@validatedForm(link = VByName('link_id'),
sort = VMenu('where', CommentSortMenu),
children = VCommentIDs('children'),
depth = VInt('depth', min = 0, max = 8),
mc_id = nop('id'))
def POST_morechildren(self, form, jquery,
link, sort, children, depth, mc_id):
user = c.user if c.user_is_loggedin else None
if not link or not link.subreddit_slow.can_view(user):
return self.abort(403,'forbidden')
if children:
builder = CommentBuilder(link, CommentSortMenu.operator(sort),
children)
items = builder.get_items(starting_depth = depth, num = 20)
def _children(cur_items):
items = []
for cm in cur_items:
items.append(cm)
if hasattr(cm, 'child'):
if hasattr(cm.child, 'things'):
items.extend(_children(cm.child.things))
cm.child = None
else:
items.append(cm.child)
return items
# assumes there is at least one child
# a = _children(items[0].child.things)
a = []
for item in items:
a.append(item)
if hasattr(item, 'child'):
a.extend(_children(item.child.things))
item.child = None
# the result is not always sufficient to replace the
# morechildren link
jquery.things(str(mc_id)).remove()
jquery.insert_things(a, append = True)
@validate(uh = nop('uh'), # VModHash() will raise, check manually
action = VOneOf('what', ('like', 'dislike', 'save')),
links = VUrl(['u']))
def GET_bookmarklet(self, action, uh, links):
'''Controller for the functionality of the bookmarklets (not
the distribution page)'''
# the redirect handler will clobber the extension if not told otherwise
c.extension = "png"
if not c.user_is_loggedin:
return self.redirect("/static/css_login.png")
# check the modhash (or force them to get new bookmarlets)
elif not c.user.valid_hash(uh) or not action:
return self.redirect("/static/css_update.png")
# unlike most cases, if not already submitted, error.
elif errors.ALREADY_SUB in c.errors:
# preserve the subreddit if not Default
sr = c.site if not isinstance(c.site, FakeSubreddit) else None
# check permissions on those links to make sure votes will count
Subreddit.load_subreddits(links, return_dict = False)
user = c.user if c.user_is_loggedin else None
links = [l for l in links if l.subreddit_slow.can_view(user)]
if links:
if action in ['like', 'dislike']:
#vote up all of the links
for link in links:
v = Vote.vote(c.user, link, action == 'like',
request.ip)
if g.write_query_queue:
queries.new_vote(v)
elif action == 'save':
link = max(links, key = lambda x: x._score)
r = link._save(c.user)
if g.write_query_queue:
queries.new_savehide(r)
return self.redirect("/static/css_%sd.png" % action)
return self.redirect("/static/css_submit.png")
@validatedForm(user = VUserWithEmail('name'))
def POST_password(self, form, jquery, user):
if not form.has_errors('name', errors.USER_DOESNT_EXIST,
errors.NO_EMAIL_FOR_USER):
emailer.password_email(user)
@validatedForm(user = VCacheKey('reset', ('key', 'name')),
password = VPassword(['passwd', 'passwd2']))
def POST_resetpassword(self, form, jquery, user, password):
if errors.BAD_USERNAME in c.errors:
return jquery.redirect('/password')
elif (not form.has_errors('passwd', errors.BAD_PASSWORD) and
not form.has_errors('passwd2', errors.BAD_PASSWORD_MATCH) and
user):
change_password(user, password)
self._login(jquery, user, '/resetpassword')
@noresponse(VUser())
def POST_noframe(self):
"""
removes the reddit toolbar if that currently the user's preference
"""
c.user.pref_frame = False
c.user._commit()
@noresponse(VUser())
def POST_frame(self):
"""
undoes POST_noframe
"""
c.user.pref_frame = True
c.user._commit()
@validatedForm()
def POST_new_captcha(self, form, jquery, *a, **kw):
jquery("body").captcha(get_iden())
@noresponse(VAdmin(),
tr = VTranslation("id"),
user = nop('user'))
def POST_deltranslator(self, tr, user):
if tr:
tr.author.remove(user)
tr.save()
@noresponse(VUser(),
VModhash(),
action = VOneOf('action', ('sub', 'unsub')),
sr = VByName('sr'))
def POST_subscribe(self, action, sr):
self._subscribe(sr, action == 'sub')
def _subscribe(self, sr, sub):
Subreddit.subscribe_defaults(c.user)
if sub:
if sr.add_subscriber(c.user):
sr._incr('_ups', 1)
else:
if sr.remove_subscriber(c.user):
sr._incr('_ups', -1)
tc.changed(sr)
@noresponse(VAdmin(),
tr = VTranslation("id"))
def POST_disable_lang(self, tr):
if tr:
tr._is_enabled = False
@noresponse(VAdmin(),
tr = VTranslation("id"))
def POST_enable_lang(self, lang):
if tr:
tr._is_enabled = True
@validatedForm(links = VByName('links', thing_cls = Link, multiple = True))
def POST_fetch_links(self, form, jquery, links):
b = IDBuilder([l._fullname for l in links],
wrap = ListingController.builder_wrapper)
l = OrganicListing(b)
l.num_margin = 0
l.mid_margin = 0
jquery.replace_things(l, stubs = True)
@noresponse(VUser(),
ui_elem = VOneOf('id', ('organic',)))
def POST_disable_ui(self, ui_elem):
if ui_elem:
pref = "pref_%s" % ui_elem
if getattr(c.user, pref):
setattr(c.user, "pref_" + ui_elem, False)
c.user._commit()
@noresponse(VSponsor(),
thing = VByName('id'))
def POST_unpromote(self, thing):
unpromote(thing)
@validatedForm(VSponsor(),
ValidDomain('url'),
ip = ValidIP(),
l = VLink('link_id'),
title = VTitle('title'),
url = VUrl(['url', 'sr'], allow_self = False),
sr = VSubmitSR('sr'),
subscribers_only = VBoolean('subscribers_only'),
disable_comments = VBoolean('disable_comments'),
expire = VOneOf('expire', ['nomodify',
'expirein', 'cancel']),
timelimitlength = VInt('timelimitlength',1,1000),
timelimittype = VOneOf('timelimittype',
['hours','days','weeks']))
def POST_edit_promo(self, form, jquery, ip,
title, url, sr, subscribers_only,
disable_comments,
expire = None,
timelimitlength = None, timelimittype = None,
l = None):
if isinstance(url, str):
# VUrl may have modified the URL to make it valid, like
# adding http://
form.set_input('url', url)
elif isinstance(url, tuple) and isinstance(url[0], Link):
# there's already one or more links with this URL, but
# we're allowing mutliple submissions, so we really just
# want the URL
url = url[0].url
if form.has_errors('title', errors.NO_TITLE):
pass
elif form.has_errors('url', errors.NO_URL, errors.BAD_URL):
pass
elif ( (not l or url != l.url) and
form.has_errors('url', errors.NO_URL, errors.ALREADY_SUB) ):
#if url == l.url, we're just editting something else
pass
elif form.has_errors('sr', errors.SUBREDDIT_NOEXIST):
pass
elif (expire == 'expirein' and
form.has_errors('timelimitlength', errors.BAD_NUMBER)):
pass
elif l:
l.title = title
old_url = l.url
l.url = url
l.is_self = False
l.promoted_subscribersonly = subscribers_only
l.disable_comments = disable_comments
if expire == 'cancel':
l.promote_until = None
elif expire == 'expirein' and timelimitlength and timelimittype:
l.promote_until = timefromnow("%d %s" % (timelimitlength,
timelimittype))
l._commit()
l.update_url_cache(old_url)
jquery.redirect('/promote/edit_promo/%s' % to36(l._id))
else:
l = Link._submit(title, url, c.user, sr, ip, False)
if expire == 'expirein' and timelimitlength and timelimittype:
promote_until = timefromnow("%d %s" % (timelimitlength,
timelimittype))
else:
promote_until = None
l._commit()
promote(l, subscribers_only = subscribers_only,
promote_until = promote_until,
disable_comments = disable_comments)
jquery.redirect('/promote/edit_promo/%s' % to36(l._id))
def GET_link_thumb(self, *a, **kw):
"""
See GET_upload_sr_image for rationale
"""
return "nothing to see here."
@validate(VSponsor(),
link = VByName('link_id'),
file = VLength('file',500*1024))
def POST_link_thumb(self, link=None, file=None):
errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "")
try:
force_thumbnail(link, file)
except cssfilter.BadImage:
# if the image doesn't clean up nicely, abort
errors["IMAGE_ERROR"] = _("bad image")
if any(errors.values()):
return UploadedImage("", "", "upload", errors = errors).render()
else:
return UploadedImage(_('saved'), thumbnail_url(link), "",
errors = errors).render()
@validatedForm(promoted = VByName('ids', thing_cls = Link, multiple = True))
def POST_onload(self, form, jquery, promoted, *a, **kw):
# make sure that they are really promoted
promoted = [ l for l in promoted if l.promoted ]
for l in promoted:
dest = l.url
jquery.set_tracker(
l._fullname,
tracking.PromotedLinkInfo.gen_url(fullname=l._fullname,
ip = request.ip),
tracking.PromotedLinkClickInfo.gen_url(fullname = l._fullname,
dest = dest,
ip = request.ip)
)