# 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 reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
# Inc. All Rights Reserved.
###############################################################################

from reddit_base import RedditController, MinimalController, set_user_cookie
from reddit_base import cross_domain, paginated_listing

from pylons.i18n import _
from pylons import c, request, response

from validator import *

from r2.models import *

from r2.lib import amqp

from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified
from r2.lib.utils import query_string, timefromnow, randstr
from r2.lib.utils import timeago, tup, filter_links
from r2.lib.pages import EnemyList, FriendList, ContributorList, ModList, \
    BannedList, BoringPage, FormPage, CssError, UploadedImage, ClickGadget, \
    UrlParser, WrappedUser, BoringPage
from r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor, \
    FlairSelector
from r2.lib.utils.trial_utils import indict, end_trial, trial_info
from r2.lib.pages.things import wrap_links, default_thing_wrapper
from r2.models.last_modified import LastModified

from r2.lib.menus import CommentSortMenu
from r2.lib.captcha import get_iden
from r2.lib.strings import strings
from r2.lib.filters import _force_unicode, websafe_json, websafe, spaceCompress
from r2.lib.db import queries
from r2.lib.db.queries import changed
from r2.lib import promote
from r2.lib.media import force_thumbnail, thumbnail_url
from r2.lib.comment_tree import delete_comment
from r2.lib import tracking,  cssfilter, emailer
from r2.lib.subreddit_search import search_reddits
from r2.lib.log import log_text
from r2.lib.filters import safemarkdown
from r2.lib.scraper import str_to_image
from r2.controllers.api_docs import api_doc, api_section

import csv
from collections import defaultdict
from datetime import datetime, timedelta
import hashlib
import urllib
import urllib2

def reject_vote(thing):
    voteword = request.params.get('dir')

    if voteword == '1':
        voteword = 'upvote'
    elif voteword == '0':
        voteword = '0-vote'
    elif voteword == '-1':
        voteword = 'downvote'

    log_text ("rejected vote", "Rejected %s from %s (%s) on %s %s via %s" %
              (voteword, c.user.name, request.ip, thing.__class__.__name__,
               thing._id36, request.referer), "info")


class ApiminimalController(MinimalController):
    """
    Put API calls in here which don't rely on the user being logged in
    """

    @validatedForm()
    @api_doc(api_section.misc)
    def POST_new_captcha(self, form, jquery, *a, **kw):
        iden = get_iden()
        jquery("body").captcha(iden)
        form._send_data(iden = iden) 


class ApiController(RedditController):
    """
    Controller which deals with almost all AJAX site interaction.  
    """

    @validatedForm()
    def ajax_login_redirect(self, form, jquery, dest):
        form.redirect("/login" + query_string(dict(dest=dest)))

    @validate(link1 = VUrl(['url']),
              link2 = VByName('id'),
              count = VLimit('limit'))
    @api_doc(api_section.links_and_comments)
    def GET_info(self, link1, link2, count):
        """
        Gets a listing of links which have the provided url.  
        """
        links = []
        if link2:
            links = filter_links(tup(link2), filter_spam = False)
        elif link1 and ('ALREADY_SUB', 'url')  in c.errors:
            links = filter_links(tup(link1), filter_spam = False)

        listing = wrap_links(filter(None, links or []), num = count)
        return BoringPage(_("API"), content = listing).render()


    @json_validate()
    @api_doc(api_section.account, extensions=["json"])
    def GET_me(self, responder):
        """
        Get info about the currently authenticated user.

        Response includes a modhash, karma, and new mail status.
        """
        if c.user_is_loggedin:
            return Wrapped(c.user).render()
        else:
            return {}

    @validatedForm(VCaptcha(),
                   name=VRequired('name', errors.NO_NAME),
                   email=ValidEmails('email', num = 1),
                   reason = VOneOf('reason', ('ad_inq', 'feedback', "i18n")),
                   message=VRequired('text', errors.NO_TEXT),
                   )
    def POST_feedback(self, form, jquery, name, email, reason, message):
        if not (form.has_errors('name',     errors.NO_NAME) or
                form.has_errors('email',    errors.BAD_EMAILS) or
                form.has_errors('text', errors.NO_TEXT) or
                form.has_errors('captcha', errors.BAD_CAPTCHA)):

            if reason == 'ad_inq':
                emailer.ad_inq_email(email, message, name, reply_to = '')
            elif reason == 'i18n':
                emailer.i18n_email(email, message, name, reply_to = '')
            else:
                emailer.feedback_email(email, message, name, reply_to = '')
            form.set_html(".status", _("thanks for your message! "
                            "you should hear back from us shortly."))
            form.set_inputs(text = "", captcha = "")
            form.find(".spacer").hide()
            form.find(".btn").hide()

    POST_ad_inq = POST_feedback


    @validatedForm(VCaptcha(),
                   VUser(),
                   VModhash(),
                   ip = ValidIP(),
                   to = VMessageRecipient('to'),
                   subject = VRequired('subject', errors.NO_SUBJECT),
                   body = VMarkdown(['text', 'message']))
    @api_doc(api_section.messages)
    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, errors.SUBREDDIT_NOEXIST,
                                errors.USER_BLOCKED) or
                form.has_errors("subject", errors.NO_SUBJECT) or
                form.has_errors("text", errors.NO_TEXT, errors.TOO_LONG) or
                form.has_errors("captcha", errors.BAD_CAPTCHA)):

            m, inbox_rel = Message._new(c.user, to, subject, body, ip)
            form.set_html(".status", _("your message has been delivered"))
            form.set_inputs(to = "", subject = "", text = "", captcha="")

            amqp.add_item('new_message', m._fullname)

            queries.new_message(m, inbox_rel)

    @validatedForm(VUser(),
                   VCaptcha(),
                   VRatelimit(rate_user = True, rate_ip = True,
                              prefix = "rate_submit_"),
                   VShamedDomain('url'),
                   ip = ValidIP(),
                   sr = VSubmitSR('sr', 'kind'),
                   url = VUrl(['url', 'sr', 'resubmit']),
                   title = VTitle('title'),
                   save = VBoolean('save'),
                   selftext = VSelfText('text'),
                   kind = VOneOf('kind', ['link', 'self']),
                   then = VOneOf('then', ('tb', 'comments'),
                                 default='comments'),
                   extension = VLength("extension", 20))
    @api_doc(api_section.links_and_comments)
    def POST_submit(self, form, jquery, url, selftext, kind, title,
                    save, sr, ip, then, extension):
        from r2.models.admintools import is_banned_domain

        if isinstance(url, (unicode, str)):
            #backwards compatability
            if url.lower() == 'self':
                url = kind = 'self'

            # VUrl may have replaced 'url' by adding 'http://'
            form.set_inputs(url = url)

        if not kind or form.has_errors('sr', errors.INVALID_OPTION):
            # this should only happen if somebody is trying to post
            # links in some automated manner outside of the regular
            # submission page, and hasn't updated their script
            return

        if form.has_errors('captcha', errors.BAD_CAPTCHA):
            return

        if (form.has_errors('sr',
                            errors.SUBREDDIT_NOEXIST,
                            errors.SUBREDDIT_NOTALLOWED,
                            errors.SUBREDDIT_REQUIRED,
                            errors.NO_SELFS,
                            errors.NO_LINKS)
            or not sr):
            # checking to get the error set in the form, but we can't
            # check for rate-limiting if there's no subreddit
            return

        if sr.link_type == 'link' and kind == 'self':
            # this could happen if they actually typed "self" into the
            # URL box and we helpfully translated it for them
            c.errors.add(errors.NO_SELFS, field='sr')

            # and trigger that by hand for the form
            form.has_errors('sr', errors.NO_SELFS)

            return

        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, 'ratelimit'))

        banmsg = None

        if kind == 'link':
            check_domain = True

            # 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.DOMAIN_BANNED):
                g.stats.simple_event('spam.shame.link')
            elif form.has_errors("url", errors.ALREADY_SUB):
                check_domain = False
                u = url[0].already_submitted_link
                if extension:
                    u = UrlParser(u)
                    u.set_extension(extension)
                    u = u.unparse()
                form.redirect(u)
            # check for title, otherwise look it up and return it
            elif form.has_errors("title", errors.NO_TEXT):
                pass

            if url is None:
                g.log.warning("%s is trying to submit url=None (title: %r)"
                              % (request.ip, title))
            elif check_domain:

                banmsg = is_banned_domain(url, request.ip)
        else:
            form.has_errors('text', errors.TOO_LONG)

        if form.has_errors("title", errors.TOO_LONG, errors.NO_TEXT):
            pass

        if form.has_errors('ratelimit', errors.RATELIMIT):
            pass

        if form.has_error() or not title:
            return

        if should_ratelimit:
            filled_quota = c.user.quota_full('link')
            if filled_quota is not None:
                if c.user._spam:
                    msg = strings.generic_quota_msg
                else:
                    log_text ("over-quota",
                              "%s just went over their per-%s quota" %
                              (c.user.name, filled_quota), "info")

                    verify_link = "/verify?reason=submit"
                    reddiquette_link = "/help/reddiquette" 

                    if c.user.email_verified:
                        msg = strings.verified_quota_msg % dict(reddiquette=reddiquette_link)
                    else:
                        msg = strings.unverified_quota_msg % dict(verify=verify_link,
                                                                  reddiquette=reddiquette_link)

                md = safemarkdown(msg)
                form.set_html(".status", md)
                c.errors.add(errors.QUOTA_FILLED)
                form.set_error(errors.QUOTA_FILLED, None)
                return

        # well, nothing left to do but submit it
        l = Link._submit(request.post.title, url if kind == 'link' else 'self',
                         c.user, sr, ip, spam=c.user._spam)

        if banmsg:
            g.stats.simple_event('spam.domainban.link_url')
            admintools.spam(l, banner = "domain (%s)" % banmsg)

        if kind == 'self':
            l.url = l.make_permalink_slow()
            l.is_self = True
            l.selftext = selftext

            l._commit()
            l.set_url_cache()

        queries.queue_vote(c.user, l, True, ip,
                           cheater = (errors.CHEATER, None) in c.errors)
        if save:
            r = l._save(c.user)
            queries.new_savehide(r)

        #set the ratelimiter
        if should_ratelimit:
            c.user.clog_quota('link', l)
            VRatelimit.ratelimit(rate_user=True, rate_ip = True,
                                 prefix = "rate_submit_")

        #update the queries
        queries.new_link(l)
        changed(l)

        if then == 'comments':
            path = add_sr(l.make_permalink_slow())
        elif then == 'tb':
            form.attr('target', '_top')
            path = add_sr('/tb/%s' % l._id36)
        if extension:
            path += ".%s" % extension
        form.redirect(path)
        form._send_data(url=path)
        form._send_data(id=l._id36)
        form._send_data(name=l._fullname)

    @validatedForm(VRatelimit(rate_ip = True,
                              rate_user = True,
                              prefix = 'fetchtitle_'),
                   VUser(),
                   url = VSanitizedUrl(['url']))
    def POST_fetch_title(self, form, jquery, url):
        if form.has_errors('ratelimit', errors.RATELIMIT):
            form.set_html(".title-status", "");
            return

        VRatelimit.ratelimit(rate_ip = True, rate_user = True,
                             prefix = 'fetchtitle_', seconds=1)
        if url:
            title = get_title(url)
            if title:
                form.set_inputs(title = title)
                form.set_html(".title-status", "");
            else:
                form.set_html(".title-status", _("no title found"))
        
    def _login(self, responder, user, 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)

        if request.params.get("hoist") != "cookie":
            responder._send_data(modhash = user.modhash())
            responder._send_data(cookie  = user.make_cookie())

    @cross_domain(allow_credentials=True)
    @api_doc(api_section.account)
    def POST_login(self, *args, **kwargs):
        return self._handle_login(*args, **kwargs)

    @cross_domain(allow_credentials=True)
    @api_doc(api_section.account)
    def POST_register(self, *args, **kwargs):
        return self._handle_register(*args, **kwargs)

    @validatedForm(user = VThrottledLogin(['user', 'passwd']),
                   rem = VBoolean('rem'))
    def _handle_login(self, form, responder, user, rem):
        if not (responder.has_errors("vdelay", errors.RATELIMIT) or
                responder.has_errors("passwd", errors.WRONG_PASSWORD)):
            self._login(responder, user, rem)

    @validatedForm(VCaptcha(),
                   VRatelimit(rate_ip = True, prefix = "rate_register_"),
                   name = VUname(['user']),
                   email = ValidEmails("email", num = 1),
                   password = VPassword(['passwd', 'passwd2']),
                   rem = VBoolean('rem'))
    def _handle_register(self, form, responder, name, email,
                      password, rem):
        bad_captcha = responder.has_errors('captcha', errors.BAD_CAPTCHA)
        if not (responder.has_errors("user", errors.BAD_USERNAME,
                                errors.USERNAME_TAKEN_DEL,
                                errors.USERNAME_TAKEN) or
                responder.has_errors("email", errors.BAD_EMAILS) or
                responder.has_errors("passwd", errors.BAD_PASSWORD) or
                responder.has_errors("passwd2", errors.BAD_PASSWORD_MATCH) or
                responder.has_errors('ratelimit', errors.RATELIMIT) or
                (not g.disable_captcha and bad_captcha)):
            
            user = register(name, password)
            VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")

            #anything else we know (email, languages)?
            if email:
                user.email = email

            user.registration_ip = request.ip
            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()

            amqp.add_item('new_account', user._fullname)

            c.user = user
            self._login(responder, user, rem)

    @noresponse(VUser(),
                VModhash(),
                container = VByName('id'))
    @api_doc(api_section.moderation)
    def POST_leavemoderator(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)
            Subreddit.special_reddits(c.user, "moderator", _update=True)
            ModAction.create(container, c.user, 'removemoderator', target=c.user, 
                             details='remove_self')

    @noresponse(VUser(),
                VModhash(),
                container = VByName('id'))
    @api_doc(api_section.moderation)
    def POST_leavecontributor(self, container):
        """
        same comment as for POST_leave_moderator.
        """
        if container and container.is_contributor(c.user):
            container.remove_contributor(c.user)
            Subreddit.special_reddits(c.user, "contributor", _update=True)

    @noresponse(VUser(),
                VModhash(),
                nuser = VExistingUname('name'),
                iuser = VByName('id'),
                container = nop('container'),
                type = VOneOf('type', ('friend', 'enemy', 'moderator', 
                                       'contributor', 'banned')))
    @api_doc(api_section.users)
    def POST_unfriend(self, nuser, iuser, container, type):
        """
        Handles removal of a friend (a user-user relation) or removal
        of a user's privileges from a subreddit (a user-subreddit
        relation).  The user can either be passed in by name (nuser)
        or by fullname (iuser).  If type is friend or enemy, 'container'
        will be the current user, otherwise the subreddit must be set.
        """
        sr_types = ('moderator', 'contributor', 'banned')
        if type in sr_types:
            container = c.site
        else:
            container = VByName('container').run(container)
            if not container:
                return

        # The user who made the request must be an admin or a moderator
        # for the privilege change to succeed.
        victim = iuser or nuser
        if (not c.user_is_admin
            and (type in sr_types and not container.is_moderator(c.user))):
            abort(403, 'forbidden')
        if (type == 'moderator' and not
            (c.user_is_admin or container.can_demod(c.user, victim))):
            abort(403, 'forbidden')
        # if we are (strictly) unfriending, the container had better
        # be the current user.
        if type in ("friend", "enemy") and container != c.user:
            abort(403, 'forbidden')
        fn = getattr(container, 'remove_' + type)
        new = fn(victim)

        # Log this action
        if new and type in sr_types:
            action = dict(banned='unbanuser', moderator='removemoderator', 
                          contributor='removecontributor').get(type, None)
            ModAction.create(container, c.user, action, target=victim)

        if type == "friend" and c.user.gold:
            c.user.friend_rels_cache(_update=True)

        if type in ("moderator", "contributor"):
            Subreddit.special_reddits(victim, type, _update=True)

    @validatedForm(VUser(),
                   VModhash(),
                   ip = ValidIP(),
                   friend = VExistingUname('name'),
                   container = nop('container'),
                   type = VOneOf('type', ('friend', 'moderator',
                                          'contributor', 'banned')),
                   note = VLength('note', 300))
    @api_doc(api_section.users)
    def POST_friend(self, form, jquery, ip, friend,
                    container, type, note):
        """
        Complement to POST_unfriend: handles friending as well as
        privilege changes on subreddits.
        """
        sr_types = ('moderator', 'contributor', 'banned')
        if type in sr_types:
            container = c.site
        else:
            container = VByName('container').run(container)
            if not container:
                return
        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 sr_types and not container.is_moderator(c.user))):
            abort(403,'forbidden')
        
        if type in sr_types and not c.user_is_admin:
            quota_key = "sr%squota-%s" % (str(type), container._id36)
            g.cache.add(quota_key, 0, time=g.sr_quota_time)
            subreddit_quota = g.cache.incr(quota_key)
            quota_limit = getattr(g, "sr_%s_quota" % type)
            if subreddit_quota > quota_limit and container.use_quotas:
                form.set_html(".status", errors.SUBREDDIT_RATELIMIT)
                c.errors.add(errors.SUBREDDIT_RATELIMIT)
                form.set_error(errors.SUBREDDIT_RATELIMIT, None)
                return

        # if we are (strictly) friending, the container
        # had better be the current user.
        if type == "friend" and container != c.user:
            abort(403,'forbidden')

        elif form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER):
            return

        new = fn(friend)

        # Log this action
        if new and type in sr_types:
            action = dict(banned='banuser', moderator='addmoderator', 
                          contributor='addcontributor').get(type, None)
            ModAction.create(container, c.user, action, target=friend)

        if type == "friend" and c.user.gold:
            # Yes, the order of the next two lines is correct.
            # First you recalculate the rel_ids, then you find
            # the right one and update its data.
            c.user.friend_rels_cache(_update=True)
            c.user.add_friend_note(friend, note or '')

        if type in ("moderator", "contributor"):
            Subreddit.special_reddits(friend, type, _update=True)

        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("#" + type + "-table").show(
                ).find("table").insert_table_rows(user_row)

            if type != 'friend' and (type != 'banned' or
                                     friend.has_interacted_with(container)):
                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
                    if isinstance(container, Subreddit):
                        title = "%s: %s" % (container.path.rstrip("/"),
                                            container.title)
                    else:
                        title = container.title
                    d = dict(url = container.path,
                             title = title)
                    msg = msg % d
                    subj = subj % d
                    if type == 'banned':
                        from_sr = True
                        sr = container
                    else:
                        from_sr = False
                        sr = None
                    item, inbox_rel = Message._new(c.user, friend, subj, msg,
                                                   ip, from_sr=from_sr, sr=sr)

                    queries.new_message(item, inbox_rel)


    @validatedForm(VGold(),
                   friend = VExistingUname('name'),
                   note = VLength('note', 300))
    def POST_friendnote(self, form, jquery, friend, note):
        c.user.add_friend_note(friend, note)
        form.set_html('.status', _("saved"))

    @validatedForm(VUser('curpass', default=''),
                   VModhash(),
                   password = VPassword(['curpass', 'curpass']),
                   dest = VDestination())
    @api_doc(api_section.account)
    def POST_clear_sessions(self, form, jquery, password, dest):
        """
        Clear all session cookies and update the current one.

        A valid password (`curpass`) must be supplied.
        """
        # password is required to proceed
        if form.has_errors("curpass", errors.WRONG_PASSWORD):
            return

        form.set_html('.status',
                      _('all other sessions have been logged out'))
        form.set_inputs(curpass = "")
        # run the change password command to get a new salt
        change_password(c.user, password)
        # the password salt has changed, so the user's cookie has been
        # invalidated.  drop a new cookie.
        self.login(c.user)

    @validatedForm(VUser('curpass', default = ''),
                   VModhash(),
                   email = ValidEmails("email", num = 1),
                   password = VPassword(['newpass', 'verpass']),
                   verify = VBoolean("verify"))
    @api_doc(api_section.account)
    def POST_update(self, form, jquery, email, password, verify):
        """
        Update account email address and password.

        Called by /prefs/update on the site. For frontend form verification
        purposes, `newpass` and `verpass` must be equal for a password change
        to succeed.
        """
        # password is required to proceed
        if form.has_errors("curpass", errors.WRONG_PASSWORD):
            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):
            if (not hasattr(c.user,'email') or c.user.email != email):
                c.user.email = email
                # unverified email for now
                c.user.email_verified = None
                c.user._commit()
                Award.take_away("verified_email", c.user)
                updated = True
            if verify:
                # TODO: rate limit this?
                emailer.verify_email(c.user, request.referer)
                form.set_html('.status',
                     _("you should be getting a verification email shortly."))
            else:
                form.set_html('.status', _('your email has been updated'))

        # user is removing their email
        if (not email and c.user.email and 
            form.has_errors("email", errors.NO_EMAILS)):
            c.user.email = ''
            c.user.email_verified = None
            c.user._commit()
            Award.take_away("verified_email", c.user)
            updated = True
            form.set_html('.status', _('your email has been updated'))

        # 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'))
            form.set_inputs(curpass = "", newpass = "", verpass = "")
            # the password has changed, so the user's cookie has been
            # invalidated.  drop a new cookie.
            self.login(c.user)

    @validatedForm(VUser(),
                   VModhash(),
                   delete_message = VLength("delete_message", max_length=500),
                   username = VRequired("user", errors.NOT_USER),
                   user = VThrottledLogin(["user", "passwd"]),
                   confirm = VBoolean("confirm"))
    @api_doc(api_section.account)
    def POST_delete_user(self, form, jquery, delete_message, username, user, confirm):
        """
        Delete an account.

        A valid username/password and confirmation must be supplied. An
        optional `delete_message` may be supplied to explain the reason the
        account is to be deleted.

        Called by /prefs/delete on the site.
        """
        if username and username.lower() != c.user.name.lower():
            c.errors.add(errors.NOT_USER, field="user")

        if not confirm:
            c.errors.add(errors.CONFIRM, field="confirm")

        if not (form.has_errors('vdelay', errors.RATELIMIT) or
                form.has_errors("user", errors.NOT_USER) or
                form.has_errors("passwd", errors.WRONG_PASSWORD) or
                form.has_errors("delete_message", errors.TOO_LONG) or
                form.has_errors("confirm", errors.CONFIRM)):
            c.user.delete(delete_message)
            form.redirect("/?deleted=true")

    @noresponse(VUser(),
                VModhash(),
                thing = VByNameIfAuthor('id'))
    @api_doc(api_section.links_and_comments)
    def POST_del(self, thing):
        if not thing: return
        '''for deleting all sorts of things'''
        was_deleted = thing._deleted
        thing._deleted = True
        if (getattr(thing, "promoted", None) is not None and
            not promote.is_promoted(thing)):
            promote.reject_promotion(thing)
        thing._commit()

        # flag search indexer that something has changed
        changed(thing)

        #expire the item from the sr cache
        if isinstance(thing, Link):
            queries.delete(thing)

        #comments have special delete tasks
        elif isinstance(thing, Comment):
            parent_id = getattr(thing, 'parent_id', None)
            link_id = thing.link_id
            recipient = None

            if parent_id:
                parent_comment = Comment._byID(parent_id, data=True)
                recipient = Account._byID(parent_comment.author_id)
            else:
                parent_link = Link._byID(link_id, data=True)
                if parent_link.is_self:
                    recipient = Account._byID(parent_link.author_id)

            if not was_deleted:
                delete_comment(thing)

            if recipient:
                inbox_class = Inbox.rel(Account, Comment)
                d = inbox_class._fast_query(recipient, thing, ("inbox", "selfreply"))
                rels = filter(None, d.values()) or None
                queries.new_comment(thing, rels)

            queries.delete(thing)

    @noresponse(VUser(),
                VModhash(),
                VSrCanAlter('id'),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_marknsfw(self, thing):
        thing.over_18 = True
        thing._commit()

        if c.user._id != thing.author_id:
            ModAction.create(thing.subreddit_slow, c.user, target=thing,
                             action='marknsfw')

        # flag search indexer that something has changed
        changed(thing)

    @noresponse(VUser(),
                VModhash(),
                VSrCanAlter('id'),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_unmarknsfw(self, thing):
        thing.over_18 = False
        thing._commit()

        if c.user._id != thing.author_id:
            ModAction.create(thing.subreddit_slow, c.user, target=thing,
                             action='marknsfw', details='remove')

        # flag search indexer that something has changed
        changed(thing)

    @noresponse(VUser(), VModhash(),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_report(self, thing):
        '''for reporting...'''
        if not thing or thing._deleted:
            return
        elif getattr(thing, 'promoted', False):
            return

        # if it is a message that is being reported, ban it.
        # every user is admin over their own personal inbox
        if isinstance(thing, Message):
            admintools.spam(thing, False, True, c.user.name)
        # auto-hide links that are reported
        elif isinstance(thing, Link):
            r = thing._hide(c.user)
            queries.new_savehide(r)
        # TODO: be nice to be able to remove comments that are reported
        # from a user's inbox so they don't have to look at them.
        elif isinstance(thing, Comment):
            pass

        if c.user._spam or c.user.ignorereports:
            return
        Report.new(c.user, thing)
        admintools.report(thing)

    @noresponse(VUser(), VModhash(),
                thing=VByName('id'))
    @api_doc(api_section.messages)
    def POST_block(self, thing):
        '''for blocking via inbox'''
        if not thing:
            return
        # Users may only block someone who has
        # actively harassed them (i.e., comment/link reply
        # or PM). Check that 'thing' would have showed up in the
        # user's inbox at some point
        if isinstance(thing, Message):
            if thing.to_id != c.user._id:
                return
        elif isinstance(thing, Comment):
            parent_id = getattr(thing, 'parent_id', None)
            link_id = thing.link_id
            if parent_id:
                parent_comment = Comment._byID(parent_id)
                parent_author_id = parent_comment.author_id
            else:
                parent_link = Link._byID(link_id)
                parent_author_id = parent_link.author_id
            if parent_author_id != c.user._id:
                return

        block_acct = Account._byID(thing.author_id)
        if block_acct.name in g.admins:
            return
        c.user.add_enemy(block_acct)

    @noresponse(VAdmin(), VModhash(),
                thing = VByName('id'))
    def POST_indict(self, thing):
        '''put something on trial'''
        if not thing:
            log_text("indict: no thing", level="warning")

        indict(thing)

    @validatedForm(VUser(),
                   VModhash(),
                   item = VByNameIfAuthor('thing_id'),
                   text = VSelfText('text'))
    @api_doc(api_section.links_and_comments)
    def POST_editusertext(self, form, jquery, item, text):
        if (not form.has_errors("text",
                                errors.NO_TEXT, errors.TOO_LONG) and
            not form.has_errors("thing_id", errors.NOT_AUTHOR)):

            if isinstance(item, Comment):
                kind = 'comment'
                item.body = text
            elif isinstance(item, Link):
                kind = 'link'
                if not getattr(item, "is_self", False):
                    return abort(403, "forbidden")
                item.selftext = text
            else:
                g.log.warning("%s tried to edit usertext on %r", c.user, item)
                return

            if item._deleted:
                return abort(403, "forbidden")

            if (item._date < timeago('3 minutes')
                or (item._ups + item._downs > 2)):
                item.editted = c.start_time

            item._commit()

            changed(item)

            amqp.add_item('usertext_edited', item._fullname)

            if kind == 'link':
                set_last_modified(item, 'comments')
                LastModified.touch(item._fullname, 'Comments')

            wrapper = default_thing_wrapper(expand_children = True)
            jquery(".content").replace_things(item, True, True, wrap = wrapper)
            jquery(".content .link .rank").hide()

    @validatedForm(VUser(),
                   VModhash(),
                   VRatelimit(rate_user = True, rate_ip = True,
                              prefix = "rate_comment_"),
                   ip = ValidIP(),
                   parent = VSubmitParent(['thing_id', 'parent']),
                   comment = VMarkdown(['text', 'comment']))
    @api_doc(api_section.links_and_comments)
    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):
            if not getattr(parent, "repliable", True):
                abort(403, 'forbidden')
            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 ((link.is_self and link.author_id == c.user._id)
                or not sr.should_ratelimit(c.user, 'comment')):
                should_ratelimit = False
            parent_age = c.start_time - parent._date
            if parent_age.days > g.REPLY_AGE_LIMIT:
                c.errors.add(errors.TOO_OLD, field = "parent")

        #remove the ratelimit error if the user's karma is high
        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        if (not commentform.has_errors("text",
                                       errors.NO_TEXT,
                                       errors.TOO_LONG) and
            not commentform.has_errors("ratelimit",
                                       errors.RATELIMIT) and
            not commentform.has_errors("parent",
                                       errors.DELETED_COMMENT,
                                       errors.DELETED_LINK,
                                       errors.TOO_OLD,
                                       errors.USER_BLOCKED)):

            if is_message:
                if parent.from_sr:
                    to = Subreddit._byID(parent.sr_id)
                else:
                    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, parent = parent)
                item.parent_id = parent._id
            else:
                item, inbox_rel = Comment._new(c.user, link, parent_comment,
                                               comment, ip)
                queries.queue_vote(c.user, item, True, ip,
                                   cheater = (errors.CHEATER, None) in c.errors)

                # adding to comments-tree is done as part of
                # newcomments_q, so if they refresh immediately they
                # won't see their comment

            # clean up the submission form and remove it from the DOM (if reply)
            t = commentform.find("textarea")
            t.attr('rows', 3).html("").val("")
            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 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', max_length = 100),
                   emails = ValidEmails("share_to"),
                   reply_to = ValidEmails("replyto", num = 1), 
                   message = VLength("message", max_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, 'ratelimit'))

        # share_from and messages share a too_long error.
        # finding an error on one necessitates hiding the other error
        if shareform.has_errors("share_from", errors.TOO_LONG):
            shareform.find(".message-errors").children().hide()
        elif shareform.has_errors("message", errors.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.TOO_MANY_EMAILS):
            shareform.find(".share-to-errors").children().hide()
        # lastly, check the captcha.
        elif shareform.has_errors("captcha", errors.BAD_CAPTCHA):
            pass
        elif shareform.has_errors("ratelimit", 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(),
                ip = ValidIP(),
                dir = VInt('dir', min=-1, max=1),
                thing = VByName('id'))
    def POST_juryvote(self, dir, thing, ip):
        if not thing:
            log_text("juryvote: no thing", level="warning")
            return

        if not ip:
            log_text("juryvote: no ip", level="warning")
            return

        if dir is None:
            log_text("juryvote: no dir", level="warning")
            return

        j = Jury.by_account_and_defendant(c.user, thing)

        if not trial_info([thing]).get(thing._fullname,False):
            log_text("juryvote: not on trial", level="warning")
            return

        if not j:
            log_text("juryvote: not on the jury", level="warning")
            return

        log_text("juryvote",
                 "%s cast a %d juryvote on %s" % (c.user.name, dir, thing._id36),
                 level="info")

        j._name = str(dir)
        j._date = c.start_time
        j._commit()

    @noresponse(VUser(),
                VModhash(),
                vote_type = VVotehash(('vh', 'id')),
                ip = ValidIP(),
                dir = VInt('dir', min=-1, max=1),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_vote(self, dir, thing, ip, vote_type):
        ip = request.ip
        user = c.user
        store = True

        if not thing or thing._deleted:
            return

        if vote_type == "rejected":
            reject_vote(thing)
            store = False

        thing_age = c.start_time - thing._date
        if thing_age.days > g.VOTE_AGE_LIMIT:
            g.log.debug("ignoring vote on old thing %s" % thing._fullname)
            store = False

        if getattr(c.user, "suspicious", False):
            g.log.info("%s cast a %d vote on %s", c.user.name, dir, thing._fullname)

        dir = (True if dir > 0
               else False if dir < 0
               else None)

        organic = vote_type == 'organic'
        queries.queue_vote(user, thing, dir, ip, organic, store = store,
                           cheater = (errors.CHEATER, None) in c.errors)

    @validatedForm(VUser(),
                   VModhash(),
                   # nop is safe: handled after auth checks below
                   stylesheet_contents = nop('stylesheet_contents'),
                   op = VOneOf('op',['save','preview']))
    @api_doc(api_section.subreddits)
    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()
            return
        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)
        jquery.apply_stylesheet(stylesheet_contents_parsed)
        if op == 'save':
            c.site.stylesheet_contents      = stylesheet_contents_parsed
            c.site.stylesheet_contents_user = stylesheet_contents

            hash = hashlib.md5(stylesheet_contents_parsed)
            c.site.stylesheet_hash = hash.hexdigest()

            set_last_modified(c.site,'stylesheet_contents')

            c.site._commit()

            ModAction.create(c.site, c.user, action='editsettings', 
                             details='stylesheet')

            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'))
    @api_doc(api_section.subreddits)
    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()
        ModAction.create(c.site, c.user, action='editsettings', 
                         details='del_image', description=name)

    @validatedForm(VSrModerator(),
                   VModhash(),
                   sponsor = VInt("sponsor", min = 0, max = 1))
    @api_doc(api_section.subreddits)
    def POST_delete_sr_header(self, form, jquery, sponsor):
        """
        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 sponsor and c.user_is_admin:
            c.site.sponsorship_img = None
            c.site.sponsorship_size = None
            c.site._commit()
        elif c.site.header:
            c.site.header = None
            c.site.header_size = None
            c.site._commit()
            ModAction.create(c.site, c.user, action='editsettings', 
                             details='del_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', max_length=1024*500),
              name = VCssName("name"),
              img_type = VImageType('img_type'),
              form_id = VLength('formid', max_length = 100), 
              header = VInt('header', max=1, min=0),
              sponsor = VSubredditSponsorship('sponsor'))
    @api_doc(api_section.subreddits)
    def POST_upload_sr_img(self, file, header, sponsor, name, form_id, img_type):
        """
        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 = "")
        add_image_to_sr = False
        size = None
        
        if not sponsor and not header:
            add_image_to_sr = True
            if not name:
                # error if the name wasn't specified and the image was not for a sponsored link or header
                # this may also fail if a sponsored image was added and the user is not an admin
                errors['BAD_CSS_NAME'] = _("bad image name")
        
        if c.site.images and add_image_to_sr:
            if c.site.get_num_images() >= g.max_sr_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:
            try:
                new_url = cssfilter.save_sr_image(c.site, file, suffix ='.' + img_type)
            except cssfilter.BadImage:
                errors['IMAGE_ERROR'] = _("Invalid image or general image error")
                return UploadedImage("", "", "", errors=errors, form_id=form_id).render()
            size = str_to_image(file).size
            if header:
                c.site.header = new_url
                c.site.header_size = size
            elif sponsor and c.user_is_admin:
                c.site.sponsorship_img = new_url
                c.site.sponsorship_size = size
            if add_image_to_sr:
                c.site.add_image(name, url = new_url)
            c.site._commit()

            if not sponsor:
                if header:
                    kw = dict(details='upload_image_header')
                else:
                    kw = dict(details='upload_image', description=name)
                ModAction.create(c.site, c.user, action='editsettings', **kw)

            return UploadedImage(_('saved'), new_url, name, 
                                 errors=errors, form_id=form_id).render()


    @validatedForm(VUser(),
                   VModhash(),
                   VRatelimit(rate_user = True,
                              rate_ip = True,
                              prefix = 'create_reddit_'),
                   sr = VByName('sr'),
                   name = VSubredditName("name"),
                   title = VLength("title", max_length = 100),
                   header_title = VLength("header-title", max_length = 500),
                   domain = VCnameDomain("domain"),
                   public_description = VMarkdown("public_description", max_length = 500),
                   description = VMarkdown("description", max_length = 5120),
                   lang = VLang("lang"),
                   over_18 = VBoolean('over_18'),
                   allow_top = VBoolean('allow_top'),
                   show_media = VBoolean('show_media'),
                   show_cname_sidebar = VBoolean('show_cname_sidebar'),
                   type = VOneOf('type', ('public', 'private', 'restricted', 'archived')),
                   link_type = VOneOf('link_type', ('any', 'link', 'self')),
                   ip = ValidIP(),
                   sponsor_text =VLength('sponsorship-text', max_length = 500),
                   sponsor_name =VLength('sponsorship-name', max_length = 64),
                   sponsor_url = VLength('sponsorship-url', max_length = 500),
                   css_on_cname = VBoolean("css_on_cname"),
                   )
    @api_doc(api_section.subreddits)
    def POST_site_admin(self, form, jquery, name, ip, sr,
                        sponsor_text, sponsor_url, sponsor_name, **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', 'show_cname_sidebar', 'type', 'link_type', 'lang',
                           "css_on_cname", "header_title", 
                           'allow_top', 'public_description'))

        #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)

        can_set_archived = c.user_is_admin or (sr and sr.type == 'archived')
        if type == 'archived' and not can_set_archived:
            form.set_error('type', errors.INVALID_OPTION)

        if not sr and form.has_errors("ratelimit", 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_TEXT, errors.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(('type', 'link_type'), errors.INVALID_OPTION) or
              form.has_errors('public_description', errors.TOO_LONG) or
              form.has_errors('description', errors.TOO_LONG)):
            pass

        #creating a new reddit
        elif not sr:
            #sending kw is ok because it was sanitized above
            sr = Subreddit._new(name = name, author_id = c.user._id, ip = ip,
                                **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/?created=true"
            if not c.user_is_admin:
                VRatelimit.ratelimit(rate_user=True,
                                     rate_ip = True,
                                     prefix = "create_reddit_")

            queries.new_subreddit(sr)
            changed(sr)

        #editting an existing reddit
        elif sr.is_moderator(c.user) or c.user_is_admin:

            if c.user_is_admin:
                sr.sponsorship_text = sponsor_text or ""
                sr.sponsorship_url = sponsor_url or None
                sr.sponsorship_name = sponsor_name or None

            #assume sr existed, or was just built
            old_domain = sr.domain

            if not sr.domain:
                del kw['css_on_cname']
            for k, v in kw.iteritems():
                if getattr(sr, k, None) != v:
                    ModAction.create(sr, c.user, action='editsettings', 
                                     details=k)
                setattr(sr, k, v)
            sr._commit()

            #update the domain cache if the domain changed
            if sr.domain != old_domain:
                Subreddit._by_domain(old_domain, _update = True)
                Subreddit._by_domain(sr.domain, _update = True)

            # flag search indexer that something has changed
            changed(sr)
            form.parent().set_html('.status', _("saved"))

        # don't go any further until the form validates
        if form.has_error():
            return
        elif redir:
            form.redirect(redir)
        else:
            jquery.refresh()

    @noresponse(q = VPrintable('q', max_length=500),
                sort = VPrintable('sort', max_length=10),
                t = VPrintable('t', max_length=10),
                approval = VBoolean('approval'))
    def POST_searchfeedback(self, q, sort, t, approval):
        timestamp = c.start_time.strftime("%Y/%m/%d-%H:%M:%S")
        if c.user_is_loggedin:
            username = c.user.name
        else:
            username = None
        d = dict(username=username, q=q, sort=sort, t=t)
        hex = hashlib.md5(repr(d)).hexdigest()
        key = "searchfeedback-%s-%s-%s" % (timestamp[:10], request.ip, hex)
        d['timestamp'] = timestamp
        d['approval'] = approval
        g.hardcache.set(key, d, time=86400 * 7)

    @noresponse(VUser(), VModhash(),
                why = VSrCanBan('id'),
                thing = VByName('id'),
                spam = VBoolean('spam', default=True))
    @api_doc(api_section.moderation)
    def POST_remove(self, why, thing, spam):

        # Don't remove a promoted link
        if getattr(thing, "promoted", None):
            return

        end_trial(thing, why + "-removed")

        filtered = thing._spam
        kw = {'target': thing}

        if filtered and spam:
            kw['details'] = 'confirm_spam'
            train_spam = False
        elif filtered and not spam:
            kw['details'] = 'remove'
            admintools.unspam(thing, unbanner=c.user.name, insert=False)
            train_spam = False
        elif not filtered and spam:
            kw['details'] = 'spam'
            train_spam = True
        elif not filtered and not spam:
            kw['details'] = 'remove'
            train_spam = False

        admintools.spam(thing, auto=False,
                        moderator_banned=not c.user_is_admin,
                        banner=c.user.name,
                        train_spam=train_spam)

        if isinstance(thing, (Link, Comment)):
            sr = thing.subreddit_slow
            action = 'remove' + thing.__class__.__name__.lower()
            ModAction.create(sr, c.user, action, **kw)

    @noresponse(VUser(), VModhash(),
                why = VSrCanBan('id'),
                thing = VByName('id'))
    @api_doc(api_section.moderation)
    def POST_approve(self, why, thing):
        if not thing: return
        if thing._deleted: return
        end_trial(thing, why + "-approved")
        kw = {'target': thing}
        if thing._spam:
            kw['details'] = 'unspam'
            train_spam = True
            insert = True
        else:
            kw['details'] = 'confirm_ham'
            train_spam = False
            insert = False

        admintools.unspam(thing, c.user.name, train_spam=train_spam,
                          insert=insert)

        if isinstance(thing, (Link, Comment)):
            sr = thing.subreddit_slow
            action = 'approve' + thing.__class__.__name__.lower()
            ModAction.create(sr, c.user, action, **kw)

    @validatedForm(VUser(), VModhash(),
                   VCanDistinguish(('id', 'how')),
                   thing = VByName('id'),
                   how = VOneOf('how', ('yes','no','admin','special')))
    @api_doc(api_section.moderation)
    def POST_distinguish(self, form, jquery, thing, how):
        if not thing:return

        log_modaction = True
        log_kw = {}
        original = thing.distinguished if hasattr(thing, 'distinguished') else 'no'
        if how == original:
            log_modaction = False   # Distinguish unchanged
        elif how in ('admin', 'special'):
            log_modaction = False   # Add admin/special
        elif original in ('admin', 'special') and how == 'no':
            log_modaction = False  # Remove admin/special
        elif how == 'no':
            log_kw['details'] = 'remove'    # yes --> no
        else:
            pass    # no --> yes

        thing.distinguished = how
        thing._commit()
        wrapper = default_thing_wrapper(expand_children = True)
        w = wrap_links(thing, wrapper)
        jquery(".content").replace_things(w, True, True)
        jquery(".content .link .rank").hide()
        if log_modaction:
            sr = thing.subreddit_slow
            ModAction.create(sr, c.user, 'distinguish', target=thing, **log_kw)

    @noresponse(VUser(),
                VModhash(),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_save(self, thing):
        if not thing: return
        r = thing._save(c.user)
        if r:
            queries.new_savehide(r)

    @noresponse(VUser(),
                VModhash(),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_unsave(self, thing):
        if not thing: return
        r = thing._unsave(c.user)
        if r:
            queries.new_savehide(r)

    def collapse_handler(self, things, collapse):
        if not things:
            return
        things = tup(things)
        srs = Subreddit._byID([t.sr_id for t in things if t.sr_id],
                              return_dict = True)
        for t in things:
            if hasattr(t, "to_id") and c.user._id == t.to_id:
                t.to_collapse = collapse
            elif hasattr(t, "author_id") and c.user._id == t.author_id:
                t.author_collapse = collapse
            elif isinstance(t, Message) and t.sr_id:
                if srs[t.sr_id].is_moderator(c.user):
                    t.to_collapse = collapse
            t._commit()

    @noresponse(VUser(),
                VModhash(),
                things = VByName('id', multiple = True))
    def POST_collapse_message(self, things):
        self.collapse_handler(things, True)

    @noresponse(VUser(),
                VModhash(),
                things = VByName('id', multiple = True))
    def POST_uncollapse_message(self, things):
        self.collapse_handler(things, False)

    def unread_handler(self, things, unread):
        if not things:
            if (errors.TOO_MANY_THING_IDS, 'id') in c.errors:
                return abort(413)
            else:
                return abort(400)

        sr_messages = defaultdict(list)
        comments = []
        messages = []
        # Group things by subreddit or type
        for thing in things:
            if isinstance(thing, Message):
                if getattr(thing, 'sr_id', False):
                    sr_messages[thing.sr_id].append(thing)
                else:
                    messages.append(thing)
            else:
                comments.append(thing)

        if sr_messages:
            mod_srs = Subreddit.reverse_moderator_ids(c.user)
            srs = Subreddit._byID(sr_messages.keys())
        else:
            mod_srs = []

        # Batch set items as unread
        for sr_id, things in sr_messages.items():
            # Remove the item(s) from the user's inbox
            queries.set_unread(things, c.user, unread)
            if sr_id in mod_srs:
                # Only moderators can change the read status of that
                # message in the modmail inbox
                sr = srs[sr_id]
                queries.set_unread(things, sr, unread)
        if comments:
            queries.set_unread(comments, c.user, unread)
        if messages:
            queries.set_unread(messages, c.user, unread)


    @noresponse(VUser(),
                VModhash(),
                things = VByName('id', multiple=True, limit=25))
    @api_doc(api_section.messages)
    def POST_unread_message(self, things):
        self.unread_handler(things, True)

    @noresponse(VUser(),
                VModhash(),
                things = VByName('id', multiple=True, limit=25))
    @api_doc(api_section.messages)
    def POST_read_message(self, things):
        self.unread_handler(things, False)

    @noresponse(VUser(),
                VModhash(),
                thing = VByName('id', thing_cls=Link))
    @api_doc(api_section.links_and_comments)
    def POST_hide(self, thing):
        if not thing: return
        r = thing._hide(c.user)
        if r:
            queries.new_savehide(r)

    @noresponse(VUser(),
                VModhash(),
                thing = VByName('id'))
    @api_doc(api_section.links_and_comments)
    def POST_unhide(self, thing):
        if not thing: return
        r = thing._unhide(c.user)
        if r:
            queries.new_savehide(r)


    @validatedForm(VUser(),
                   parent = VByName('parent_id'))
    def POST_moremessages(self, form, jquery, parent):
        if not parent.can_view_slow():
            return self.abort(403,'forbidden')

        if parent.sr_id:
            builder = SrMessageBuilder(parent.subreddit_slow,
                                       parent = parent, skip = False)
        else:
            builder = UserMessageBuilder(c.user, parent = parent, skip = False)
        listing = Listing(builder).listing()
        a = []
        for item in listing.things:
            a.append(item)
            for x in item.child.things:
                a.append(x)
        for item in a:
            if hasattr(item, "child"):
                item.child = None
        jquery.things(parent._fullname).parent().replace_things(a, False, True)

    @validatedForm(link = VByName('link_id'),
                   sort = VMenu('where', CommentSortMenu),
                   children = VCommentIDs('children'),
                   pv_hex = VPrintable('pv_hex', 40),
                   mc_id = nop('id'))
    @api_doc(api_section.links_and_comments)
    def POST_morechildren(self, form, jquery, link, sort, children,
                          pv_hex, mc_id):
        user = c.user if c.user_is_loggedin else None

        mc_key = "morechildren-%s" % request.ip
        try:
            count = g.cache.incr(mc_key)
        except:
            g.cache.set(mc_key, 1, time=30)
            count = 1

        # Anything above 15 hits in 30 seconds violates the
        # "1 request per 2 seconds" rule of the API
        if count > 15:
            if user:
                name = user.name
            else:
                name = "(unlogged user)"
            g.log.warning("%s on %s hit morechildren %d times in 30 seconds"
                          % (name, request.ip, count))
            # TODO: redirect to rickroll or something

        if not link or not link.subreddit_slow.can_view(user):
            return abort(403,'forbidden')

        if pv_hex:
            c.previous_visits = g.cache.get(pv_hex)

        if children:
            builder = CommentBuilder(link, CommentSortMenu.operator(sort),
                                     children)
            listing = Listing(builder, nextprev = False)
            items = listing.get_items(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)

            if pv_hex:
                jquery.rehighlight_new_comments()


    @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:
                        queries.queue_vote(c.user, link,
                                           action == 'like', request.ip,
                                           cheater = (errors.CHEATER, None) in c.errors)
                elif action == 'save':
                    link = max(links, key = lambda x: x._score)
                    r = link._save(c.user)
                    queries.new_savehide(r)
                return self.redirect("/static/css_%sd.png" % action)
        return self.redirect("/static/css_submit.png")


    @validatedForm(VUser(),
                   code = VPrintable("code", 30))
    def POST_claimgold(self, form, jquery, code):
        if not code:
            c.errors.add(errors.NO_TEXT, field = "code")
            form.has_errors("code", errors.NO_TEXT)
            return

        rv = claim_gold(code, c.user._id)

        if rv is None:
            c.errors.add(errors.INVALID_CODE, field = "code")
            log_text ("invalid gold claim",
                      "%s just tried to claim %s" % (c.user.name, code),
                      "info")
        elif rv == "already claimed":
            c.errors.add(errors.CLAIMED_CODE, field = "code")
            log_text ("invalid gold reclaim",
                      "%s just tried to reclaim %s" % (c.user.name, code),
                      "info")
        else:
            days, subscr_id = rv
            if days <= 0:
                raise ValueError("days = %r?" % days)

            log_text ("valid gold claim",
                      "%s just claimed %s" % (c.user.name, code),
                      "info")

            if subscr_id:
                c.user.gold_subscr_id = subscr_id

            if code.startswith("cr_"):
                c.user.gold_creddits += int(days / 31)
                c.user._commit()
                form.set_html(".status", _("claimed! now go to someone's userpage and give them a present!"))
            else:
                admintools.engolden(c.user, days)

                g.cache.set("recent-gold-" + c.user.name, True, 600)
                form.set_html(".status", _("claimed!"))
                jquery(".lounge").show()

        # Activate any errors we just manually set
        form.has_errors("code", errors.INVALID_CODE, errors.CLAIMED_CODE,
                        errors.NO_TEXT)

    @validatedForm(user = VUserWithEmail('name'))
    def POST_password(self, form, jquery, user):
        if form.has_errors('name', errors.USER_DOESNT_EXIST):
            return
        elif form.has_errors('name', errors.NO_EMAIL_FOR_USER):
            return
        else:
            if emailer.password_email(user):
                form.set_html(".status",
                      _("an email will be sent to that account's address shortly"))
            else:
                form.set_html(".status", _("try again tomorrow"))


    @validatedForm(token=VOneTimeToken(PasswordResetToken, "key"),
                   password=VPassword(["passwd", "passwd2"]))
    def POST_resetpassword(self, form, jquery, token, password):
        # was the token invalid or has it expired?
        if not token:
            form.redirect("/password?expired=true")
            return

        # did they fill out the password form correctly?
        form.has_errors("passwd",  errors.BAD_PASSWORD)
        form.has_errors("passwd2", errors.BAD_PASSWORD_MATCH)
        if form.has_error():
            return

        # at this point, we should mark the token used since it's either
        # valid now or will never be valid again.
        token.consume()

        # load up the user and check that things haven't changed
        user = Account._by_fullname(token.user_id)
        if not token.valid_for_user(user):
            form.redirect('/password?expired=true')
            return

        # successfully entered user name and valid new password
        change_password(user, password)
        g.log.warning("%s did a password reset for %s via %s",
                      request.ip, user.name, token._id)

        self._login(jquery, user)
        jquery.redirect('/')


    @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()


    @noresponse(VUser(),
                VModhash(),
                action = VOneOf('action', ('sub', 'unsub')),
                sr = VSubscribeSR('sr', 'sr_name'))
    @api_doc(api_section.subreddits)
    def POST_subscribe(self, action, sr):
        # only users who can make edits are allowed to subscribe.
        # Anyone can leave.
        if sr and (action != 'sub' or sr.can_comment(c.user)):
            self._subscribe(sr, action == 'sub')

    @classmethod
    def _subscribe(cls, sr, sub):
        try:
            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)
            changed(sr, True)
        except CreationError:
            # This only seems to happen when someone is pounding on the
            # subscribe button or the DBs are really lagged; either way,
            # some other proc has already handled this subscribe request.
            return


    @validatedForm(VAdmin(),
                   hexkey=VLength("hexkey", max_length=32),
                   nickname=VLength("nickname", max_length = 1000),
                   status = VOneOf("status",
                      ("new", "severe", "interesting", "normal", "fixed")))
    def POST_edit_error(self, form, jquery, hexkey, nickname, status):
        if form.has_errors(("hexkey", "nickname", "status"),
                           errors.NO_TEXT, errors.INVALID_OPTION):
            pass

        if form.has_error():
            return

        key = "error_nickname-%s" % str(hexkey)
        g.hardcache.set(key, nickname, 86400 * 365)

        key = "error_status-%s" % str(hexkey)
        g.hardcache.set(key, status, 86400 * 365)

        form.set_html(".status", _('saved'))

    @validatedForm(VSponsor(),
                   ad = VByName("fullname"),
                   colliding_ad=VAdByCodename(("codename", "fullname")),
                   codename = VLength("codename", max_length = 100),
                   imgurl = VLength("imgurl", max_length = 1000),
                   raw_html = VLength("raw_html", max_length = 10000),
                   linkurl = VLength("linkurl", max_length = 1000))
    def POST_editad(self, form, jquery, ad, colliding_ad, codename,
                    imgurl, raw_html, linkurl):
        if form.has_errors(("codename", "imgurl", "linkurl"),
                           errors.NO_TEXT):
            pass

        if form.has_errors(("codename"), errors.INVALID_OPTION):
            form.set_html(".status", "some other ad has that codename")
            pass

        if form.has_error():
            return

        if ad is None:
            Ad._new(codename,
                    imgurl=imgurl,
                    raw_html=raw_html,
                    linkurl=linkurl)
            form.set_html(".status", "saved. reload to see it.")
            return

        ad.codename = codename
        ad.imgurl = imgurl
        ad.raw_html = raw_html
        ad.linkurl = linkurl
        ad._commit()
        form.set_html(".status", _('saved'))

    @validatedForm(VSponsor(),
                   ad = VByName("fullname"),
                   sr = VSubmitSR("community"),
                   weight = VInt("weight",
                                 coerce=False, min=0, max=100000),
                   )
    def POST_assignad(self, form, jquery, ad, sr, weight):
        if form.has_errors("ad", errors.NO_TEXT):
            pass

        if form.has_errors("community", errors.SUBREDDIT_REQUIRED,
            errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED):
            pass

        if form.has_errors("fullname", errors.NO_TEXT):
            pass

        if form.has_errors("weight", errors.BAD_NUMBER):
            pass

        if form.has_error():
            return

        if ad.codename == "DART" and sr.name == g.default_sr and weight != 100:
            log_text("Bad default DART weight",
                     "The default DART weight can only be 100, not %s."
                     % weight,
                     "error")
            abort(403, 'forbidden')

        existing = AdSR.by_ad_and_sr(ad, sr)

        if weight is not None:
            if existing:
                existing.weight = weight
                existing._commit()
            else:
                AdSR._new(ad, sr, weight)

            form.set_html(".status", _('saved'))

        else:
            if existing:
                existing._delete()
                AdSR.by_ad(ad, _update=True)
                AdSR.by_sr(sr, _update=True)

            form.set_html(".status", _('deleted'))


    @validatedForm(VAdmin(),
                   award=VByName("fullname"),
                   colliding_award=VAwardByCodename(("codename", "fullname")),
                   codename=VLength("codename", max_length = 100),
                   title=VLength("title", max_length = 100),
                   awardtype=VOneOf("awardtype",
                                    ("regular", "manual", "invisible")),
                   api_ok=VBoolean("api_ok"),
                   imgurl=VLength("imgurl", max_length = 1000))
    def POST_editaward(self, form, jquery, award, colliding_award, codename,
                       title, awardtype, api_ok, imgurl):
        if form.has_errors(("codename", "title", "awardtype", "imgurl"),
                           errors.NO_TEXT):
            pass

        if awardtype is None:
            form.set_html(".status", "bad awardtype")
            return

        if form.has_errors(("codename"), errors.INVALID_OPTION):
            form.set_html(".status", "some other award has that codename")
            pass

        if form.has_error():
            return

        if award is None:
            Award._new(codename, title, awardtype, imgurl, api_ok)
            form.set_html(".status", "saved. reload to see it.")
            return

        award.codename = codename
        award.title = title
        award.awardtype = awardtype
        award.imgurl = imgurl
        award.api_ok = api_ok
        award._commit()
        form.set_html(".status", _('saved'))

    @validatedForm(VFlairManager(),
                   VModhash(),
                   user = VFlairAccount("name"),
                   link = VFlairLink('link'),
                   text = VFlairText("text"),
                   css_class = VFlairCss("css_class"))
    @api_doc(api_section.flair)
    def POST_flair(self, form, jquery, user, link, text, css_class):
        if link:
            flair_type = LINK_FLAIR
            if hasattr(c.site, '_id') and c.site._id == link.sr_id:
                site = c.site
            else:
                site = Subreddit._byID(link.sr_id, data=True)
                # make sure c.user has permission to set flair on this link
                if not c.user_is_admin and not site.is_moderator(c.user):
                    abort(403, 'forbidden')
        else:
            flair_type = USER_FLAIR
            site = c.site
            if form.has_errors('name', errors.BAD_FLAIR_TARGET):
                return

        if form.has_errors('css_class', errors.BAD_CSS_NAME):
            form.set_html(".status:first", _('invalid css class'))
            return
        if form.has_errors('css_class', errors.TOO_MUCH_FLAIR_CSS):
            form.set_html(".status:first", _('too many css classes'))
            return

        if flair_type == LINK_FLAIR:
            if not text and not css_class:
                text = css_class = None
            link.flair_text = text
            link.flair_css_class = css_class
            link._commit()
            changed(link)
            ModAction.create(site, c.user, action='editflair', target=link,
                             details='flair_edit')
        elif flair_type == USER_FLAIR:
            if not text and not css_class:
                # empty text and css is equivalent to unflairing
                text = css_class = None
                c.site.remove_flair(user)
                jquery('#flairrow_%s' % user._id36).hide()
                new = False
            elif not c.site.is_flair(user):
                c.site.add_flair(user)
                new = True
            else:
                new = False

            # Save the flair details in the account data.
            setattr(user, 'flair_%s_text' % c.site._id, text)
            setattr(user, 'flair_%s_css_class' % c.site._id, css_class)
            user._commit()

            if c.user != user:
                ModAction.create(site, c.user, action='editflair',
                                 target=user, details='flair_edit')

            if new:
                jquery.redirect('?name=%s' % user.name)
            else:
                flair = WrappedUser(
                    user, force_show_flair=True,
                    include_flair_selector=True).render(style='html')
                jquery('.tagline .flairselectable.id-%s'
                    % user._fullname).parent().html(flair)
                jquery('input[name="text"]').data('saved', text)
                jquery('input[name="css_class"]').data('saved', css_class)
                form.set_html('.status', _('saved'))

    @validatedForm(VFlairManager(),
                   VModhash(),
                   user = VFlairAccount("name"))
    @api_doc(api_section.flair)
    def POST_deleteflair(self, form, jquery, user):
        # Check validation.
        if form.has_errors('name', errors.USER_DOESNT_EXIST, errors.NO_USER):
            return
        c.site.remove_flair(user)
        setattr(user, 'flair_%s_text' % c.site._id, None)
        setattr(user, 'flair_%s_css_class' % c.site._id, None)
        user._commit()

        ModAction.create(c.site, c.user, action='editflair', target=user,
                         details='flair_delete')

        jquery('#flairrow_%s' % user._id36).remove()
        unflair = WrappedUser(
            user, include_flair_selector=True).render(style='html')
        jquery('.tagline .id-%s' % user._fullname).parent().html(unflair)

    @validate(VFlairManager(),
              VModhash(),
              flair_csv = nop('flair_csv'))
    @api_doc(api_section.flair)
    def POST_flaircsv(self, flair_csv):
        limit = 100  # max of 100 flair settings per call
        results = FlairCsv()
        # encode to UTF-8, since csv module doesn't fully support unicode
        infile = csv.reader(flair_csv.strip().encode('utf-8').split('\n'))
        for i, row in enumerate(infile):
            line_result = results.add_line()
            line_no = i + 1
            if line_no > limit:
                line_result.error('row',
                                  'limit of %d rows per call reached' % limit)
                break

            try:
                name, text, css_class = row
            except ValueError:
                line_result.error('row', 'improperly formatted row, ignoring')
                continue

            user = VFlairAccount('name').run(name)
            if not user:
                line_result.error('user',
                                  "unable to resolve user `%s', ignoring"
                                  % name)
                continue

            if not text and not css_class:
                # this is equivalent to unflairing
                text = None
                css_class = None

            orig_text = text
            text = VFlairText('text').run(orig_text)
            if text and orig_text and len(text) < len(orig_text):
                line_result.warn('text',
                                 'truncating flair text to %d chars'
                                 % len(text))

            if css_class and not VFlairCss('css_class').run(css_class):
                line_result.error('css',
                                  "invalid css class `%s', ignoring"
                                  % css_class)
                continue

            # all validation passed, enflair the user
            if text or css_class:
                mode = 'added'
                c.site.add_flair(user)
            else:
                mode = 'removed'
                c.site.remove_flair(user)
            setattr(user, 'flair_%s_text' % c.site._id, text)
            setattr(user, 'flair_%s_css_class' % c.site._id, css_class)
            user._commit()

            line_result.status = '%s flair for user %s' % (mode, user.name)
            line_result.ok = True

        ModAction.create(c.site, c.user, action='editflair',
                         details='flair_csv')

        return BoringPage(_("API"), content = results).render()

    @validatedForm(VUser(),
                   VModhash(),
                   flair_enabled = VBoolean("flair_enabled"))
    @api_doc(api_section.flair)
    def POST_setflairenabled(self, form, jquery, flair_enabled):
        setattr(c.user, 'flair_%s_enabled' % c.site._id, flair_enabled)
        c.user._commit()
        jquery.refresh()

    @validatedForm(
        VFlairManager(),
        VModhash(),
        flair_enabled = VBoolean("flair_enabled"),
        flair_position = VOneOf("flair_position", ("left", "right")),
        link_flair_position = VOneOf("link_flair_position",
                                     ("", "left", "right")),
        flair_self_assign_enabled = VBoolean("flair_self_assign_enabled"),
        link_flair_self_assign_enabled =
            VBoolean("link_flair_self_assign_enabled"))
    @api_doc(api_section.flair)
    def POST_flairconfig(self, form, jquery, flair_enabled, flair_position,
                         link_flair_position, flair_self_assign_enabled,
                         link_flair_self_assign_enabled):
        if c.site.flair_enabled != flair_enabled:
            c.site.flair_enabled = flair_enabled
            ModAction.create(c.site, c.user, action='editflair',
                             details='flair_enabled')
        if c.site.flair_position != flair_position:
            c.site.flair_position = flair_position
            ModAction.create(c.site, c.user, action='editflair',
                             details='flair_position')
        if c.site.link_flair_position != link_flair_position:
            c.site.link_flair_position = link_flair_position
            ModAction.create(c.site, c.user, action='editflair',
                             details='link_flair_position')
        if c.site.flair_self_assign_enabled != flair_self_assign_enabled:
            c.site.flair_self_assign_enabled = flair_self_assign_enabled
            ModAction.create(c.site, c.user, action='editflair',
                             details='flair_self_enabled')
        if (c.site.link_flair_self_assign_enabled
            != link_flair_self_assign_enabled):
            c.site.link_flair_self_assign_enabled = (
                link_flair_self_assign_enabled)
            ModAction.create(c.site, c.user, action='editflair',
                             details='link_flair_self_enabled')
        c.site._commit()
        jquery.refresh()

    @paginated_listing(max_page_size=1000)
    @validate(user = VFlairAccount('name'))
    @api_doc(api_section.flair)
    def GET_flairlist(self, num, after, reverse, count, user):
        flair = FlairList(num, after, reverse, '', user)
        return BoringPage(_("API"), content = flair).render()

    @validatedForm(VFlairManager(),
                   VModhash(),
                   flair_template = VFlairTemplateByID('flair_template_id'),
                   text = VFlairText('text'),
                   css_class = VFlairCss('css_class'),
                   text_editable = VBoolean('text_editable'),
                   flair_type = VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),
                                       default=USER_FLAIR))
    @api_doc(api_section.flair)
    def POST_flairtemplate(self, form, jquery, flair_template, text,
                           css_class, text_editable, flair_type):
        if text is None:
            text = ''
        if css_class is None:
            css_class = ''

        # Check validation.
        if form.has_errors('css_class', errors.BAD_CSS_NAME):
            form.set_html(".status:first", _('invalid css class'))
            return
        if form.has_errors('css_class', errors.TOO_MUCH_FLAIR_CSS):
            form.set_html(".status:first", _('too many css classes'))
            return

        # Load flair template thing.
        if flair_template:
            flair_template.text = text
            flair_template.css_class = css_class
            flair_template.text_editable = text_editable
            flair_template._commit()
            new = False
        else:
            try:
                flair_template = FlairTemplateBySubredditIndex.create_template(
                    c.site._id, text=text, css_class=css_class,
                    text_editable=text_editable,
                    flair_type=flair_type)
            except OverflowError:
                form.set_html(".status:first", _('max flair templates reached'))
                return

            new = True

        # Push changes back to client.
        if new:
            empty_ids = {
                USER_FLAIR: '#empty-user-flair-template',
                LINK_FLAIR: '#empty-link-flair-template',
            }
            empty_id = empty_ids[flair_type]
            jquery(empty_id).before(
                FlairTemplateEditor(flair_template, flair_type)
                .render(style='html'))
            empty_template = FlairTemplate()
            empty_template._committed = True  # to disable unnecessary warning
            jquery(empty_id).html(
                FlairTemplateEditor(empty_template, flair_type)
                .render(style='html'))
            form.set_html('.status', _('saved'))
        else:
            jquery('#%s' % flair_template._id).html(
                FlairTemplateEditor(flair_template, flair_type)
                .render(style='html'))
            form.set_html('.status', _('saved'))
            jquery('input[name="text"]').data('saved', text)
            jquery('input[name="css_class"]').data('saved', css_class)
        ModAction.create(c.site, c.user, action='editflair',
                             details='flair_template')

    @validatedForm(VFlairManager(),
                   VModhash(),
                   flair_template = VFlairTemplateByID('flair_template_id'))
    @api_doc(api_section.flair)
    def POST_deleteflairtemplate(self, form, jquery, flair_template):
        idx = FlairTemplateBySubredditIndex.by_sr(c.site._id)
        if idx.delete_by_id(flair_template._id):
            jquery('#%s' % flair_template._id).parent().remove()
            ModAction.create(c.site, c.user, action='editflair',
                             details='flair_delete_template')

    @validatedForm(VFlairManager(), VModhash(),
                   flair_type = VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),
                                       default=USER_FLAIR))
    @api_doc(api_section.flair)
    def POST_clearflairtemplates(self, form, jquery, flair_type):
        FlairTemplateBySubredditIndex.clear(c.site._id, flair_type=flair_type)
        jquery.refresh()
        ModAction.create(c.site, c.user, action='editflair',
                         details='flair_clear_template')

    @validate(VUser(),
              user = VFlairAccount('name'),
              link = VFlairLink('link'))
    def POST_flairselector(self, user, link):
        if link:
            if hasattr(c.site, '_id') and c.site._id == link.sr_id:
                site = c.site
            else:
                site = Subreddit._byID(link.sr_id, data=True)
            return FlairSelector(link=link, site=site).render()
        if user and not (c.user_is_admin or c.site.is_moderator(c.user)):
            # ignore user parameter if c.user is not mod/admin
            user = None
        return FlairSelector(user=user).render()

    @validatedForm(VUser(),
                   VModhash(),
                   user = VFlairAccount('name'),
                   link = VFlairLink('link'),
                   flair_template_id = nop('flair_template_id'),
                   text = VFlairText('text'))
    @api_doc(api_section.flair)
    def POST_selectflair(self, form, jquery, user, link, flair_template_id,
                         text):
        if link:
            flair_type = LINK_FLAIR
            if hasattr(c.site, '_id') and c.site._id == link.sr_id:
                site = c.site
            else:
                site = Subreddit._byID(link.sr_id, data=True)
            self_assign_enabled = site.link_flair_self_assign_enabled
        else:
            flair_type = USER_FLAIR
            site = c.site
            self_assign_enabled = site.flair_self_assign_enabled

        if flair_template_id:
            try:
                flair_template = FlairTemplateBySubredditIndex.get_template(
                    site._id, flair_template_id, flair_type=flair_type)
            except NotFound:
                # TODO: serve error to client
                g.log.debug('invalid flair template for subreddit %s', site._id)
                return
        else:
            flair_template = None
            text = None

        if not site.is_moderator(c.user) and not c.user_is_admin:
            if not self_assign_enabled:
                # TODO: serve error to client
                g.log.debug('flair self-assignment not permitted')
                return

            # Ignore user choice if not an admin or mod.
            user = c.user

            # Ignore given text if user doesn't have permission to customize it.
            if not (flair_template and flair_template.text_editable):
                text = None

        if not text:
            text = flair_template.text if flair_template else None

        css_class = flair_template.css_class if flair_template else None
        text_editable = (
            flair_template.text_editable if flair_template else False)

        if flair_type == USER_FLAIR:
            site.add_flair(user)
            setattr(user, 'flair_%s_text' % site._id, text)
            setattr(user, 'flair_%s_css_class' % site._id, css_class)
            user._commit()

            if ((c.site.is_moderator(c.user) or c.user_is_admin)
                and c.user != user):
                ModAction.create(c.site, c.user, action='editflair',
                                 target=user, details='flair_edit')

            # Push some client-side updates back to the browser.
            u = WrappedUser(user, force_show_flair=True,
                            flair_text_editable=text_editable,
                            include_flair_selector=True)
            flair = u.render(style='html')
            jquery('.tagline .flairselectable.id-%s'
                % user._fullname).parent().html(flair)
            jquery('#flairrow_%s input[name="text"]' % user._id36).data(
                'saved', text).val(text)
            jquery('#flairrow_%s input[name="css_class"]' % user._id36).data(
                'saved', css_class).val(css_class)
        elif flair_type == LINK_FLAIR:
            link.flair_text = text
            link.flair_css_class = css_class
            link._commit()
            changed(link)

            if ((c.site.is_moderator(c.user) or c.user_is_admin)):
                ModAction.create(c.site, c.user, action='editflair',
                                 target=link, details='flair_edit')

            # Push some client-side updates back to the browser.

            jquery('.id-%s .entry .linkflair' % link._fullname).remove()
            title_path = '.id-%s .entry > .title > .title' % link._fullname

            # TODO: move this to a template
            if flair_template:
                flair = '<span class="linkflair %s">%s</span>' % (
                    ' '.join('linkflair-' + c for c in css_class.split()), text)
                if c.site.link_flair_position == 'left':
                    jquery(title_path).before(flair)
                elif c.site.link_flair_position == 'right':
                    jquery(title_path).after(flair)

            # TODO: close the selector popup more gracefully
            jquery('body').click()

    @validatedForm(secret_used=VAdminOrAdminSecret("secret"),
                   award=VByName("fullname"),
                   description=VLength("description", max_length=1000),
                   url=VLength("url", max_length=1000),
                   cup_hours=VFloat("cup_hours",
                                      coerce=False, min=0, max=24 * 365),
                   recipient=VExistingUname("recipient"))
    def POST_givetrophy(self, form, jquery, secret_used, award, description,
                        url, cup_hours, recipient):
        if form.has_errors("recipient", errors.USER_DOESNT_EXIST,
                                        errors.NO_USER):
            pass

        if form.has_errors("fullname", errors.NO_TEXT, errors.NO_THING_ID):
            pass

        if form.has_errors("cup_hours", errors.BAD_NUMBER):
            pass
        
        if secret_used and not award.api_ok:
            c.errors.add(errors.NO_API, field='secret')
            form.has_errors('secret', errors.NO_API)

        if form.has_error():
            return

        if cup_hours:
            cup_seconds = int(cup_hours * 3600)
            cup_expiration = timefromnow("%s seconds" % cup_seconds)
        else:
            cup_expiration = None
        
        t = Trophy._new(recipient, award, description=description, url=url,
                        cup_info=dict(expiration=cup_expiration))

        form.set_html(".status", _('saved'))
        form._send_data(trophy_fn=t._id36)
    
    @validatedForm(VAdmin(),
                   account = VExistingUname("account"))
    def POST_removecup(self, form, jquery, account):
        if not account:
            return self.abort404()
        account.remove_cup()

    @validatedForm(secret_used=VAdminOrAdminSecret("secret"),
                   trophy = VTrophy("trophy_fn"))
    def POST_removetrophy(self, form, jquery, secret_used, trophy):
        if not trophy:
            return self.abort404()
        recipient = trophy._thing1
        award = trophy._thing2
        if secret_used and not award.api_ok:
            c.errors.add(errors.NO_API, field='secret')
            form.has_errors('secret', errors.NO_API)
        
        if form.has_error():
            return

        trophy._delete()
        Trophy.by_account(recipient, _update=True)
        Trophy.by_award(award, _update=True)

    @validatedForm(links = VByName('links', thing_cls = Link, multiple = True),
                   show = VByName('show', thing_cls = Link, multiple = False))
    def POST_fetch_links(self, form, jquery, links, show):
        l = wrap_links(links, listing_cls = SpotlightListing,
                       num_margin = 0, mid_margin = 0)
        jquery(".content").replace_things(l, stubs = True)

        if show:
            jquery('.organic-listing .link:visible').hide()
            jquery('.organic-listing .id-%s' % show._fullname).show()

    @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()

    @validatedForm(type = VOneOf('type', ('click'), default = 'click'),
                   links = VByName('ids', thing_cls = Link, multiple = True))
    def GET_gadget(self, form, jquery, type, links):
        if not links and type == 'click':
            # malformed cookie, clear it out
            set_user_cookie('recentclicks2', '')

        if not links:
            return

        content = ClickGadget(links).make_content()

        jquery('.gadget').show().find('.click-gadget').html(
            spaceCompress(content))

    @noresponse()
    def POST_tb_commentspanel_show(self):
        # this preference is allowed for non-logged-in users
        c.user.pref_frame_commentspanel = True
        c.user._commit()

    @noresponse()
    def POST_tb_commentspanel_hide(self):
        # this preference is allowed for non-logged-in users
        c.user.pref_frame_commentspanel = False
        c.user._commit()

    @json_validate(query = VPrintable('query', max_length = 50))
    def POST_search_reddit_names(self, responder, query):
        names = []
        if query:
            names = search_reddits(query)

        return {'names': names}

    @validate(link = VByName('link_id', thing_cls = Link))
    def POST_expando(self, link):
        if not link:
            abort(404, 'not found')

        wrapped = wrap_links(link)
        wrapped = list(wrapped)[0]
        return websafe(spaceCompress(wrapped.link_child.content()))

    @validatedForm(VUser('password', default=''),
                   VModhash(),
                   VOneTimePassword("otp",
                                    required=not g.disable_require_admin_otp),
                   remember=VBoolean("remember"),
                   dest=VDestination())
    def POST_adminon(self, form, jquery, remember, dest):
        if form.has_errors('password', errors.WRONG_PASSWORD):
            return

        if form.has_errors("otp", errors.WRONG_PASSWORD,
                                  errors.NO_OTP_SECRET,
                                  errors.RATELIMIT):
            return

        if remember:
            self.remember_otp(c.user)

        self.enable_admin_mode(c.user)
        form.redirect(dest)

    @validatedForm(VUser("password", default=""),
                   VModhash())
    def POST_generate_otp_secret(self, form, jquery):
        if form.has_errors("password", errors.WRONG_PASSWORD):
            return

        secret = totp.generate_secret()
        g.cache.set('otp_secret_' + c.user._id36, secret, time=300)
        jquery("body").make_totp_qrcode(secret)

    @validatedForm(VUser(),
                   VModhash(),
                   otp=nop("otp"))
    def POST_enable_otp(self, form, jquery, otp):
        if form.has_errors("password", errors.WRONG_PASSWORD):
            return

        secret = g.cache.get("otp_secret_" + c.user._id36)
        if not secret:
            c.errors.add(errors.EXPIRED, field="otp")
            form.has_errors("otp", errors.EXPIRED)
            return

        if not VOneTimePassword.validate_otp(secret, otp):
            c.errors.add(errors.WRONG_PASSWORD, field="otp")
            form.has_errors("otp", errors.WRONG_PASSWORD)
            return

        c.user.otp_secret = secret
        c.user._commit()

        form.redirect("/prefs/otp")

    @validatedForm(VUser("password", default=""),
                   VOneTimePassword("otp", required=True),
                   VModhash())
    def POST_disable_otp(self, form, jquery):
        if form.has_errors("password", errors.WRONG_PASSWORD):
            return

        if form.has_errors("otp", errors.WRONG_PASSWORD,
                                  errors.NO_OTP_SECRET,
                                  errors.RATELIMIT):
            return

        c.user.otp_secret = ""
        c.user._commit()
        form.redirect("/prefs/otp")
