# 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-2015 reddit
# Inc. All Rights Reserved.
###############################################################################

import cgi
import json

from pylons import c, g, request, response
from pylons.i18n import _
from pylons.controllers.util import abort
from r2.config.extensions import api_type
from r2.lib import utils, captcha, promote, totp, ratelimit
from r2.lib.filters import unkeep_space, websafe, _force_unicode
from r2.lib.filters import markdown_souptest
from r2.lib.db import tdb_cassandra
from r2.lib.db.operators import asc, desc
from r2.lib.template_helpers import add_sr
from r2.lib.jsonresponse import JQueryResponse, JsonResponse
from r2.lib.log import log_text
from r2.lib.permissions import ModeratorPermissionSet
from r2.models import *
from r2.models.promo import Location
from r2.lib.authorize import Address, CreditCard
from r2.lib.utils import constant_time_compare, make_offset_date
from r2.lib.require import require, require_split, RequirementException

from r2.lib.errors import errors, RedditError, UserRequiredException
from r2.lib.errors import VerifiedUserRequiredException

from copy import copy
from datetime import datetime, timedelta
from curses.ascii import isprint
import re, inspect
from itertools import chain
from functools import wraps

def visible_promo(article):
    is_promo = getattr(article, "promoted", None) is not None
    is_author = (c.user_is_loggedin and
                 c.user._id == article.author_id)

    # promos are visible only if comments are not disabled and the
    # user is either the author or the link is live/previously live.
    if is_promo:
        return (c.user_is_sponsor or
                is_author or
                (not article.disable_comments and
                 article.promote_status >= PROMOTE_STATUS.promoted))
    # not a promo, therefore it is visible
    return True

def can_view_link_comments(article):
    return (article.subreddit_slow.can_view(c.user) and
            visible_promo(article))

def can_comment_link(article):
    return (article.subreddit_slow.can_comment(c.user) and
            visible_promo(article))

class Validator(object):
    notes = None
    default_param = None
    def __init__(self, param=None, default=None, post=True, get=True, url=True,
                 body=False, docs=None):
        if param:
            self.param = param
        else:
            self.param = self.default_param

        self.default = default
        self.post, self.get, self.url, self.docs = post, get, url, docs
        self.body = body
        self.has_errors = False

    def set_error(self, error, msg_params={}, field=False, code=None):
        """
        Adds the provided error to c.errors and flags that it is come
        from the validator's param
        """
        if field is False:
            field = self.param

        c.errors.add(error, msg_params=msg_params, field=field, code=code)
        self.has_errors = True

    def param_docs(self):
        param_info = {}
        for param in filter(None, tup(self.param)):
            param_info[param] = None
        return param_info

    def __call__(self, url):
        self.has_errors = False
        a = []
        if self.param:
            for p in utils.tup(self.param):
                # cgi.FieldStorage is falsy even if it has a filled value
                # property. :(
                post_val = request.POST.get(p)
                if self.post and (post_val or
                                  isinstance(post_val, cgi.FieldStorage)):
                    val = request.POST[p]
                elif self.get and request.GET.get(p):
                    val = request.GET[p]
                elif self.url and url.get(p):
                    val = url[p]
                elif self.body:
                    val = request.body
                else:
                    val = self.default
                a.append(val)
        try:
            return self.run(*a)
        except TypeError, e:
            if str(e).startswith('run() takes'):
                # Prepend our class name so we know *which* run()
                raise TypeError('%s.%s' % (type(self).__name__, str(e)))
            else:
                raise


def build_arg_list(fn, env):
    """given a fn and and environment the builds a keyword argument list
    for fn"""
    kw = {}
    argspec = inspect.getargspec(fn)

    # if there is a **kw argument in the fn definition,
    # just pass along the environment
    if argspec[2]:
        kw = env
    #else for each entry in the arglist set the value from the environment
    else:
        #skip self
        argnames = argspec[0][1:]
        for name in argnames:
            if name in env:
                kw[name] = env[name]
    return kw

def _make_validated_kw(fn, simple_vals, param_vals, env):
    for validator in simple_vals:
        validator(env)
    kw = build_arg_list(fn, env)
    for var, validator in param_vals.iteritems():
        kw[var] = validator(env)
    return kw

def set_api_docs(fn, simple_vals, param_vals, extra_vals=None):
    doc = fn._api_doc = getattr(fn, '_api_doc', {})
    param_info = doc.get('parameters', {})
    notes = doc.get('notes', [])
    for validator in chain(simple_vals, param_vals.itervalues()):
        param_docs = validator.param_docs()
        if validator.docs:
            param_docs.update(validator.docs)
        param_info.update(param_docs)
        if validator.notes:
            notes.append(validator.notes)
    if extra_vals:
        param_info.update(extra_vals)
    doc['parameters'] = param_info
    doc['notes'] = notes

def _validators_handle_csrf(simple_vals, param_vals):
    for validator in chain(simple_vals, param_vals.itervalues()):
        if getattr(validator, 'handles_csrf', False):
            return True
    return False

def validate(*simple_vals, **param_vals):
    """Validation decorator that delegates error handling to the controller.

    Runs the validators specified and calls self.on_validation_error to
    process each error. This allows controllers to define their own fatal
    error processing logic.
    """
    def val(fn):
        @wraps(fn)
        def newfn(self, *a, **env):
            try:
                kw = _make_validated_kw(fn, simple_vals, param_vals, env)
            except RedditError as err:
                self.on_validation_error(err)

            for err in c.errors:
                self.on_validation_error(c.errors[err])

            try:
                return fn(self, *a, **kw)
            except RedditError as err:
                self.on_validation_error(err)

        set_api_docs(newfn, simple_vals, param_vals)
        newfn.handles_csrf = _validators_handle_csrf(simple_vals, param_vals)
        return newfn
    return val


def api_validate(response_type=None, add_api_type_doc=False):
    """
    Factory for making validators for API calls, since API calls come
    in two flavors: responsive and unresponsive.  The machinary
    associated with both is similar, and the error handling identical,
    so this function abstracts away the kw validation and creation of
    a Json-y responder object.
    """
    def wrap(response_function):
        def _api_validate(*simple_vals, **param_vals):
            def val(fn):
                @wraps(fn)
                def newfn(self, *a, **env):
                    renderstyle = request.params.get("renderstyle")
                    if renderstyle:
                        c.render_style = api_type(renderstyle)
                    elif not c.extension:
                        # if the request URL included an extension, don't
                        # touch the render_style, since it was already set by
                        # set_extension. if no extension was provided, default
                        # to response_type.
                        c.render_style = api_type(response_type)

                    # generate a response object
                    if response_type == "html" and not request.params.get('api_type') == "json":
                        responder = JQueryResponse()
                    else:
                        responder = JsonResponse()

                    response.content_type = responder.content_type

                    try:
                        kw = _make_validated_kw(fn, simple_vals, param_vals, env)
                        return response_function(self, fn, responder,
                                                 simple_vals, param_vals, *a, **kw)
                    except UserRequiredException:
                        responder.send_failure(errors.USER_REQUIRED)
                        return self.api_wrapper(responder.make_response())
                    except VerifiedUserRequiredException:
                        responder.send_failure(errors.VERIFIED_USER_REQUIRED)
                        return self.api_wrapper(responder.make_response())

                extra_param_vals = {}
                if add_api_type_doc:
                    extra_param_vals = {
                        "api_type": "the string `json`",
                    }

                set_api_docs(newfn, simple_vals, param_vals, extra_param_vals)
                newfn.handles_csrf = _validators_handle_csrf(simple_vals,
                                                             param_vals)
                return newfn
            return val
        return _api_validate
    return wrap


@api_validate("html")
def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw):
    self_method(self, *a, **kw)
    return self.api_wrapper({})

@api_validate("html")
def textresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw):
    return self_method(self, *a, **kw)

@api_validate()
def json_validate(self, self_method, responder, simple_vals, param_vals, *a, **kw):
    if c.extension != 'json':
        abort(404)

    val = self_method(self, responder, *a, **kw)
    if val is None:
        val = responder.make_response()
    return self.api_wrapper(val)

def _validatedForm(self, self_method, responder, simple_vals, param_vals,
                  *a, **kw):
    # generate a form object
    form = responder(request.POST.get('id', "body"))

    # clear out the status line as a courtesy
    form.set_text(".status", "")

    # do the actual work
    val = self_method(self, form, responder, *a, **kw)

    # add data to the output on some errors
    for validator in chain(simple_vals, param_vals.values()):
        if (isinstance(validator, VCaptcha) and
            (form.has_errors('captcha', errors.BAD_CAPTCHA) or
             (form.has_error() and c.user.needs_captcha()))):
            form.new_captcha()
        elif (isinstance(validator, (VRatelimit, VThrottledLogin)) and
              form.has_errors('ratelimit', errors.RATELIMIT)):
            form.ratelimit(validator.seconds)
    if val:
        return val
    else:
        return self.api_wrapper(responder.make_response())

@api_validate("html", add_api_type_doc=True)
def validatedForm(self, self_method, responder, simple_vals, param_vals,
                  *a, **kw):
    return _validatedForm(self, self_method, responder, simple_vals, param_vals,
                          *a, **kw)

@api_validate("html", add_api_type_doc=True)
def validatedMultipartForm(self, self_method, responder, simple_vals,
                           param_vals, *a, **kw):
    def wrapped_self_method(*a, **kw):
        val = self_method(*a, **kw)
        if val:
            return val
        else:
            data = json.dumps(responder.make_response())
            response.content_type = "text/html"
            return ('<html><head><script type="text/javascript">\n'
                    'parent.$.handleResponse().call('
                    'parent.$("#" + window.frameElement.id).parent(), %s)\n'
                    '</script></head></html>') % filters.websafe_json(data)
    return _validatedForm(self, wrapped_self_method, responder, simple_vals,
                          param_vals, *a, **kw)


jsonp_callback_rx = re.compile(r"""\A[\w$\."'[\]]+\Z""")
def valid_jsonp_callback(callback):
    return jsonp_callback_rx.match(callback)


#### validators ####
class nop(Validator):
    def run(self, x):
        return x

class VLang(Validator):
    @staticmethod
    def validate_lang(lang, strict=False):
        if lang in g.all_languages:
            return lang
        else:
            if not strict:
                return g.lang
            else:
                raise ValueError("invalid language %r" % lang)
    def run(self, lang):
        return VLang.validate_lang(lang)

    def param_docs(self):
        return {
            self.param: "a valid IETF language tag (underscore separated)",
        }


class VRequired(Validator):
    def __init__(self, param, error, *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self._error = error

    def error(self, e = None):
        if not e: e = self._error
        if e:
            self.set_error(e)

    def run(self, item):
        if not item:
            self.error()
        else:
            return item

class VThing(Validator):
    def __init__(self, param, thingclass, redirect = True, *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self.thingclass = thingclass
        self.redirect = redirect

    def run(self, thing_id):
        if thing_id:
            try:
                tid = int(thing_id, 36)
                thing = self.thingclass._byID(tid, True)
                if thing.__class__ != self.thingclass:
                    raise TypeError("Expected %s, got %s" %
                                    (self.thingclass, thing.__class__))
                return thing
            except (NotFound, ValueError):
                if self.redirect:
                    abort(404, 'page not found')
                else:
                    return None

class VLink(VThing):
    def __init__(self, param, redirect = True, *a, **kw):
        VThing.__init__(self, param, Link, redirect=redirect, *a, **kw)

class VPromoCampaign(VThing):
    def __init__(self, param, redirect = True, *a, **kw):
        VThing.__init__(self, param, PromoCampaign, *a, **kw)

class VCommentByID(VThing):
    def __init__(self, param, redirect = True, *a, **kw):
        VThing.__init__(self, param, Comment, redirect=redirect, *a, **kw)


class VAward(VThing):
    def __init__(self, param, redirect = True, *a, **kw):
        VThing.__init__(self, param, Award, redirect=redirect, *a, **kw)

class VAwardByCodename(Validator):
    def run(self, codename, required_fullname=None):
        if not codename:
            return self.set_error(errors.NO_TEXT)

        try:
            a = Award._by_codename(codename)
        except NotFound:
            a = None

        if a and required_fullname and a._fullname != required_fullname:
            return self.set_error(errors.INVALID_OPTION)
        else:
            return a

class VTrophy(VThing):
    def __init__(self, param, redirect = True, *a, **kw):
        VThing.__init__(self, param, Trophy, redirect=redirect, *a, **kw)

class VMessage(Validator):
    def run(self, message_id):
        if message_id:
            try:
                aid = int(message_id, 36)
                return Message._byID(aid, True)
            except (NotFound, ValueError):
                abort(404, 'page not found')


class VCommentID(Validator):
    def run(self, cid):
        if cid:
            try:
                cid = int(cid, 36)
                return Comment._byID(cid, True)
            except (NotFound, ValueError):
                pass

class VMessageID(Validator):
    def run(self, cid):
        if cid:
            try:
                cid = int(cid, 36)
                m = Message._byID(cid, True)
                if not m.can_view_slow():
                    abort(403, 'forbidden')
                return m
            except (NotFound, ValueError):
                pass

class VCount(Validator):
    def run(self, count):
        if count is None:
            count = 0
        try:
            return max(int(count), 0)
        except ValueError:
            return 0

    def param_docs(self):
        return {
            self.param: "a positive integer (default: 0)",
        }


class VLimit(Validator):
    def __init__(self, param, default=25, max_limit=100, **kw):
        self.default_limit = default
        self.max_limit = max_limit
        Validator.__init__(self, param, **kw)

    def run(self, limit):
        default = c.user.pref_numsites
        if not default or c.render_style in ("compact", api_type("compact")):
            default = self.default_limit  # TODO: ini param?

        if limit is None:
            return default

        try:
            i = int(limit)
        except ValueError:
            return default

        return min(max(i, 1), self.max_limit)

    def param_docs(self):
        return {
            self.param: "the maximum number of items desired "
                        "(default: %d, maximum: %d)" % (self.default_limit,
                                                        self.max_limit),
        }

class VCssMeasure(Validator):
    measure = re.compile(r"\A\s*[\d\.]+\w{0,3}\s*\Z")
    def run(self, value):
        return value if value and self.measure.match(value) else ''

subreddit_rx = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\Z")
language_subreddit_rx = re.compile(r"\A[a-z]{2}\Z")

def chksrname(x, allow_language_srs=False, allow_special_srs=False):
    if not x:
        return None

    #notice the space before reddit.com
    if not allow_special_srs and x in ('friends', 'all', ' reddit.com'):
        return False

    try:
        valid = subreddit_rx.match(x)
        if allow_language_srs:
            valid = valid or language_subreddit_rx.match(x)

        return str(x) if valid else None
    except UnicodeEncodeError:
        return None


class VLength(Validator):
    only_whitespace = re.compile(r"\A\s*\Z", re.UNICODE)

    def __init__(self, param, max_length,
                 min_length=0,
                 empty_error = errors.NO_TEXT,
                 length_error = errors.TOO_LONG,
                 short_error=errors.TOO_SHORT,
                 **kw):
        Validator.__init__(self, param, **kw)
        self.max_length = max_length
        self.min_length = min_length
        self.length_error = length_error
        self.short_error = short_error
        self.empty_error = empty_error

    def run(self, text, text2 = ''):
        text = text or text2
        if self.empty_error and (not text or self.only_whitespace.match(text)):
            self.set_error(self.empty_error, code=400)
        elif len(text) > self.max_length:
            self.set_error(self.length_error, {'max_length': self.max_length}, code=400)
        elif len(text) < self.min_length:
            self.set_error(self.short_error, {'min_length': self.min_length},
                           code=400)
        else:
            return text

    def param_docs(self):
        return {
            self.param:
                "a string no longer than %d characters" % self.max_length,
        }

class VUploadLength(VLength):
    def run(self, upload, text2=''):
        # upload is expected to be a FieldStorage object
        if isinstance(upload, cgi.FieldStorage):
            return VLength.run(self, upload.value, text2)
        else:
            self.set_error(self.empty_error, code=400)

    def param_docs(self):
        kibibytes = self.max_length / 1024
        return {
            self.param:
                "file upload with maximum size of %d KiB" % kibibytes,
        }

class VPrintable(VLength):
    def run(self, text, text2 = ''):
        text = VLength.run(self, text, text2)

        if text is None:
            return None

        try:
            if all(isprint(str(x)) for x in text):
                return str(text)
        except UnicodeEncodeError:
            pass

        self.set_error(errors.BAD_STRING, code=400)
        return None

    def param_docs(self):
        return {
            self.param: "a string up to %d characters long,"
                        " consisting of printable characters."
                            % self.max_length,
        }

class VTitle(VLength):
    def __init__(self, param, max_length = 300, **kw):
        VLength.__init__(self, param, max_length, **kw)

    def param_docs(self):
        return {
            self.param: "title of the submission. "
                        "up to %d characters long" % self.max_length,
        }

class VMarkdown(Validator):
    def __init__(self, param, renderer='reddit'):
        Validator.__init__(self, param)
        self.renderer = renderer

    def run(self, text, text2=''):
        text = text or text2
        try:
            markdown_souptest(text, renderer=self.renderer)
            return text
        except ValueError:
            import sys
            user = "???"
            if c.user_is_loggedin:
                user = c.user.name
            g.log.error("HAX by %s: %s" % (user, text))
            s = sys.exc_info()
            # reraise the original error with the original stack trace
            raise s[1], None, s[2]

    def param_docs(self):
        return {
            tup(self.param)[0]: "raw markdown text",
        }


class VMarkdownLength(VMarkdown):
    def __init__(self, param, renderer='reddit', max_length=10000,
                 empty_error=errors.NO_TEXT, length_error=errors.TOO_LONG):
        VMarkdown.__init__(self, param, renderer)
        self.max_length = max_length
        self.empty_error = empty_error
        self.length_error = length_error

    def run(self, text, text2=''):
        text = text or text2
        text = VLength(self.param, self.max_length,
                       empty_error=self.empty_error,
                       length_error=self.length_error).run(text)
        if text:
            return VMarkdown.run(self, text)
        else:
            return ''


class VSavedCategory(Validator):
    savedcategory_rx = re.compile(r"\A[a-z0-9 _]{1,20}\Z")

    def run(self, name):
        if not name:
            return
        name = name.lower()
        valid = self.savedcategory_rx.match(name)
        if not valid:
            self.set_error('BAD_SAVE_CATEGORY')
            return
        return name

    def param_docs(self):
        return {
            self.param: "a category name",
        }


class VSubredditName(VRequired):
    def __init__(self, item, allow_language_srs=False, *a, **kw):
        VRequired.__init__(self, item, errors.BAD_SR_NAME, *a, **kw)
        self.allow_language_srs = allow_language_srs

    def run(self, name):
        name = chksrname(name, self.allow_language_srs)
        if not name:
            self.set_error(self._error, code=400)
        return name

    def param_docs(self):
        return {
            self.param: "subreddit name",
        }


class VAvailableSubredditName(VSubredditName):
    def run(self, name):
        name = VSubredditName.run(self, name)
        if name:
            try:
                a = Subreddit._by_name(name)
                return self.error(errors.SUBREDDIT_EXISTS)
            except NotFound:
                return name


class VSRByName(Validator):
    def run(self, sr_name):
        if not sr_name:
            self.set_error(errors.BAD_SR_NAME, code=400)
        else:
            try:
                sr = Subreddit._by_name(sr_name)
                return sr
            except NotFound:
                self.set_error(errors.SUBREDDIT_NOEXIST, code=400)

    def param_docs(self):
        return {
            self.param: "subreddit name",
        }


class VSRByNames(Validator):
    """Returns a dict mapping subreddit names to subreddit objects.

    sr_names_csv - a comma delimited string of subreddit names
    required - if true (default) an empty subreddit name list is an error

    """
    def __init__(self, sr_names_csv, required=True):
        self.required = required
        Validator.__init__(self, sr_names_csv)

    def run(self, sr_names_csv):
        if sr_names_csv:
            sr_names = [s.strip() for s in sr_names_csv.split(',')]
            return Subreddit._by_name(sr_names)
        elif self.required:
            self.set_error(errors.BAD_SR_NAME, code=400)
        return {}

    def param_docs(self):
        return {
            self.param: "comma-delimited list of subreddit names",
        }


class VSubredditTitle(Validator):
    def run(self, title):
        if not title:
            self.set_error(errors.NO_TITLE)
        elif len(title) > 100:
            self.set_error(errors.TITLE_TOO_LONG)
        else:
            return title

class VSubredditDesc(Validator):
    def run(self, description):
        if description and len(description) > 500:
            self.set_error(errors.DESC_TOO_LONG)
        return unkeep_space(description or '')

class VAccountByName(VRequired):
    def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw):
        VRequired.__init__(self, param, error, *a, **kw)

    def run(self, name):
        if name:
            try:
                return Account._by_name(name)
            except NotFound: pass
        return self.error()

    def param_docs(self):
        return {self.param: "A valid, existing reddit username"}


class VFriendOfMine(VAccountByName):
    def run(self, name):
        # Must be logged in
        VUser().run()
        maybe_friend = VAccountByName.run(self, name)
        if maybe_friend:
            friend_rel = Account.get_friend(c.user, maybe_friend)
            if friend_rel:
                return friend_rel
            else:
                self.error(errors.NOT_FRIEND)
        return None


def fullname_regex(thing_cls = None, multiple = False):
    pattern = "[%s%s]" % (Relation._type_prefix, Thing._type_prefix)
    if thing_cls:
        pattern += utils.to36(thing_cls._type_id)
    else:
        pattern += r"[0-9a-z]+"
    pattern += r"_[0-9a-z]+"
    if multiple:
        pattern = r"(%s *,? *)+" % pattern
    return re.compile(r"\A" + pattern + r"\Z")

class VByName(Validator):
    # Lookup tdb_sql.Thing or tdb_cassandra.Thing objects by fullname.
    splitter = re.compile('[ ,]+')
    def __init__(self, param, thing_cls=None, multiple=False, limit=None,
                 error=errors.NO_THING_ID, ignore_missing=False,
                 backend='sql', **kw):
        # Limit param only applies when multiple is True
        if not multiple and limit is not None:
            raise TypeError('multiple must be True when limit is set')
        self.thing_cls = thing_cls
        self.re = fullname_regex(thing_cls)
        self.multiple = multiple
        self.limit = limit
        self._error = error
        self.ignore_missing = ignore_missing
        self.backend = backend

        Validator.__init__(self, param, **kw)

    def run(self, items):
        if self.backend == 'cassandra':
            # tdb_cassandra.Thing objects can't use the regex
            if items and self.multiple:
                items = [item for item in self.splitter.split(items)]
                if self.limit and len(items) > self.limit:
                    return self.set_error(errors.TOO_MANY_THING_IDS)
            if items:
                try:
                    return tdb_cassandra.Thing._by_fullname(
                        items,
                        ignore_missing=self.ignore_missing,
                        return_dict=False,
                    )
                except tdb_cassandra.NotFound:
                    pass
        else:
            if items and self.multiple:
                items = [item for item in self.splitter.split(items)
                         if item and self.re.match(item)]
                if self.limit and len(items) > self.limit:
                    return self.set_error(errors.TOO_MANY_THING_IDS)
            if items and (self.multiple or self.re.match(items)):
                try:
                    return Thing._by_fullname(
                        items,
                        return_dict=False,
                        ignore_missing=self.ignore_missing,
                        data=True,
                    )
                except NotFound:
                    pass

        return self.set_error(self._error)

    def param_docs(self):
        thingtype = (self.thing_cls or Thing).__name__.lower()
        if self.multiple:
            return {
                self.param: ("A comma-separated list of %s [fullnames]"
                             "(#fullnames)" % thingtype)
            }
        else:
            return {
                self.param: "[fullname](#fullnames) of a %s" % thingtype,
            }

class VByNameIfAuthor(VByName):
    def run(self, fullname):
        thing = VByName.run(self, fullname)
        if thing:
            if not thing._loaded: thing._load()
            if c.user_is_loggedin and thing.author_id == c.user._id:
                return thing
        return self.set_error(errors.NOT_AUTHOR)

    def param_docs(self):
        return {
            self.param: "[fullname](#fullnames) of a thing created by the user",
        }

class VCaptcha(Validator):
    default_param = ('iden', 'captcha')

    def run(self, iden, solution):
        if c.user.needs_captcha():
            valid_captcha = captcha.valid_solution(iden, solution)
            if not valid_captcha:
                self.set_error(errors.BAD_CAPTCHA)
            g.stats.action_event_count("captcha", valid_captcha)

    def param_docs(self):
        return {
            self.param[0]: "the identifier of the CAPTCHA challenge",
            self.param[1]: "the user's response to the CAPTCHA challenge",
        }


class VRegistrationCaptcha(VCaptcha):
    def run(self, iden, solution):
        if not feature.is_enabled('new_login_flow'):
            return VCaptcha.run(self, iden, solution)


class VUser(Validator):
    def run(self, password = None):
        if not c.user_is_loggedin:
            raise UserRequiredException

        if (password is not None) and not valid_password(c.user, password):
            self.set_error(errors.WRONG_PASSWORD)

class VModhash(Validator):
    handles_csrf = True
    default_param = 'uh'
    def __init__(self, param=None, fatal=True, *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self.fatal = fatal

    def run(self, uh):
        if uh is None:
            uh = request.headers.get('X-Modhash')

        if not c.user_is_loggedin or uh != c.user.name:
            if self.fatal:
                abort(403)
            self.set_error('INVALID_MODHASH')

    def param_docs(self):
        return {
            '%s / X-Modhash header' % self.param: 'a [modhash](#modhashes)',
        }


class VModhashIfLoggedIn(Validator):
    handles_csrf = True
    default_param = 'uh'

    def __init__(self, param=None, fatal=True, *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self.fatal = fatal

    def run(self, uh):
        # import here so that we don't close around VModhash
        # before r2admin can override
        if c.user_is_loggedin:
            from r2.lib.validator import VModhash
            VModhash(fatal=self.fatal).run(uh)

    def param_docs(self):
        return {
            '%s / X-Modhash header' % self.param: 'a [modhash](#modhashes)',
        }


class VVotehash(Validator):
    def run(self, vh):
        return None

    def param_docs(self):
        return {
            self.param[0]: "ignored",
        }

class VAdmin(Validator):
    def run(self):
        if not c.user_is_admin:
            abort(404, "page not found")

def make_or_admin_secret_cls(base_cls):
    class VOrAdminSecret(base_cls):
        handles_csrf = True

        def run(self, secret=None):
            '''If validation succeeds, return True if the secret was used,
            False otherwise'''
            if secret and constant_time_compare(secret,
                                                g.secrets["ADMINSECRET"]):
                return True
            super(VOrAdminSecret, self).run()

            # import here so that we don't close around VModhash
            # before r2admin can override
            if request.method.upper() != "GET":
                from r2.lib.validator import VModhash
                VModhash(fatal=True).run(request.POST.get("uh"))

            return False
    return VOrAdminSecret

VAdminOrAdminSecret = make_or_admin_secret_cls(VAdmin)

class VVerifiedUser(VUser):
    def run(self):
        VUser.run(self)
        if not c.user.email_verified:
            raise VerifiedUserRequiredException

class VGold(VUser):
    notes = "*Requires a subscription to [reddit gold](/gold/about)*"
    def run(self):
        VUser.run(self)
        if not c.user.gold:
            abort(403, 'forbidden')

class VSponsorAdmin(VVerifiedUser):
    """
    Validator which checks c.user_is_sponsor
    """
    def user_test(self, thing):
        return (thing.author_id == c.user._id)

    def run(self, link_id = None):
        VVerifiedUser.run(self)
        if c.user_is_sponsor:
            return
        abort(403, 'forbidden')

VSponsorAdminOrAdminSecret = make_or_admin_secret_cls(VSponsorAdmin)

class VSponsor(VVerifiedUser):
    """
    Not intended to be used as a check for c.user_is_sponsor, but
    rather is the user allowed to use the sponsored link system.
    If a link or campaign is passed in, it also checks whether the user is
    allowed to edit that particular sponsored link.
    """
    def user_test(self, thing):
        return (thing.author_id == c.user._id)

    def run(self, link_id=None, campaign_id=None):
        assert not (link_id and campaign_id), 'Pass link or campaign, not both'

        VVerifiedUser.run(self)
        if c.user_is_sponsor:
            return
        elif campaign_id:
            pc = None
            try:
                if '_' in campaign_id:
                    pc = PromoCampaign._by_fullname(campaign_id, data=True)
                else:
                    pc = PromoCampaign._byID36(campaign_id, data=True)
            except (NotFound, ValueError):
                pass
            if pc:
                link_id = pc.link_id
        if link_id:
            try:
                if '_' in link_id:
                    t = Link._by_fullname(link_id, True)
                else:
                    aid = int(link_id, 36)
                    t = Link._byID(aid, True)
                if self.user_test(t):
                    return
            except (NotFound, ValueError):
                pass
            abort(403, 'forbidden')


class VEmployee(VVerifiedUser):
    """Validate that user is an employee."""
    def run(self):
        if not c.user.employee:
            abort(403, 'forbidden')
        VVerifiedUser.run(self)


class VSrModerator(Validator):
    def __init__(self, fatal=True, perms=(), *a, **kw):
        # If True, abort rather than setting an error
        self.fatal = fatal
        self.perms = utils.tup(perms)
        super(VSrModerator, self).__init__(*a, **kw)

    def run(self):
        if not (c.user_is_loggedin
                and c.site.is_moderator_with_perms(c.user, *self.perms)
                or c.user_is_admin):
            if self.fatal:
                abort(403, "forbidden")
            return self.set_error('MODERATOR_REQUIRED', code=403)

class VCanDistinguish(VByName):
    def run(self, thing_name, how):
        if c.user_is_loggedin:
            item = VByName.run(self, thing_name)
            if item.author_id == c.user._id:
                # will throw a legitimate 500 if this isn't a link or
                # comment, because this should only be used on links and
                # comments
                subreddit = item.subreddit_slow
                if how in ("yes", "no") and subreddit.can_distinguish(c.user):
                    return True
                elif how in ("special", "no") and c.user_special_distinguish:
                    return True
                elif how in ("admin", "no") and c.user.employee:
                    return True

        abort(403,'forbidden')

    def param_docs(self):
        return {}

class VSrCanAlter(VByName):
    def run(self, thing_name):
        if c.user_is_admin:
            return True
        elif c.user_is_loggedin:
            item = VByName.run(self, thing_name)
            if item.author_id == c.user._id:
                return True
            elif item.promoted and c.user_is_sponsor:
                return True
            else:
                # will throw a legitimate 500 if this isn't a link or
                # comment, because this should only be used on links and
                # comments
                subreddit = item.subreddit_slow
                if subreddit.can_distinguish(c.user):
                    return True
        abort(403,'forbidden')

class VSrCanBan(VByName):
    def run(self, thing_name):
        if c.user_is_admin:
            return True
        elif c.user_is_loggedin:
            item = VByName.run(self, thing_name)
            if isinstance(item, (Link, Comment)):
                sr = item.subreddit_slow
                if sr.is_moderator_with_perms(c.user, 'posts'):
                    return True
            elif isinstance(item, Message):
                sr = item.subreddit_slow
                if sr and sr.is_moderator_with_perms(c.user, 'mail'):
                    return True
        abort(403,'forbidden')

class VSrSpecial(VByName):
    def run(self, thing_name):
        if c.user_is_admin:
            return True
        elif c.user_is_loggedin:
            item = VByName.run(self, thing_name)
            # will throw a legitimate 500 if this isn't a link or
            # comment, because this should only be used on links and
            # comments
            subreddit = item.subreddit_slow
            if subreddit.is_special(c.user):
                return True
        abort(403,'forbidden')


class VSubmitParent(VByName):
    def run(self, fullname, fullname2):
        #for backwards compatability (with iphone app)
        fullname = fullname or fullname2
        if fullname:
            parent = VByName.run(self, fullname)
            if not isinstance(parent, (Comment, Link, Message)):
                abort(403, "forbidden")

            if parent:
                if c.user_is_loggedin and parent.author_id in c.user.enemies:
                    self.set_error(errors.USER_BLOCKED)
                if parent._deleted:
                    if isinstance(parent, Link):
                        self.set_error(errors.DELETED_LINK)
                    else:
                        self.set_error(errors.DELETED_COMMENT)
                if parent._spam and isinstance(parent, Comment):
                    # Only author, mod or admin can reply to removed comments
                    can_reply = (c.user_is_loggedin and
                                 (parent.author_id == c.user._id or
                                  c.user_is_admin or
                                  parent.subreddit_slow.is_moderator(c.user)))
                    if not can_reply:
                        self.set_error(errors.DELETED_COMMENT)
            if isinstance(parent, Message):
                return parent
            else:
                link = parent
                if isinstance(parent, Comment):
                    link = Link._byID(parent.link_id, data=True)
                if link and c.user_is_loggedin and can_comment_link(link):
                    return parent
        #else
        abort(403, "forbidden")

    def param_docs(self):
        return {
            self.param[0]: "[fullname](#fullnames) of parent thing",
        }

class VSubmitSR(Validator):
    def __init__(self, srname_param, linktype_param=None, promotion=False):
        self.require_linktype = False
        self.promotion = promotion

        if linktype_param:
            self.require_linktype = True
            Validator.__init__(self, (srname_param, linktype_param))
        else:
            Validator.__init__(self, srname_param)

    def run(self, sr_name, link_type = None):
        if not sr_name:
            self.set_error(errors.SUBREDDIT_REQUIRED)
            return None

        try:
            sr = Subreddit._by_name(str(sr_name).strip())
        except (NotFound, AttributeError, UnicodeEncodeError):
            self.set_error(errors.SUBREDDIT_NOEXIST)
            return

        if not c.user_is_loggedin or not sr.can_submit(c.user, self.promotion):
            self.set_error(errors.SUBREDDIT_NOTALLOWED)
            return

        if self.require_linktype:
            if link_type not in ('link', 'self'):
                self.set_error(errors.INVALID_OPTION)
                return
            elif link_type == "link" and not sr.can_submit_link(c.user):
                self.set_error(errors.NO_LINKS)
                return
            elif link_type == "self" and not sr.can_submit_text(c.user):
                self.set_error(errors.NO_SELFS)
                return

        return sr

    def param_docs(self):
        return {
            self.param[0]: "name of a subreddit",
        }

class VSubscribeSR(VByName):
    def __init__(self, srid_param, srname_param):
        VByName.__init__(self, (srid_param, srname_param))

    def run(self, sr_id, sr_name):
        if sr_id:
            return VByName.run(self, sr_id)
        elif not sr_name:
            return

        try:
            sr = Subreddit._by_name(str(sr_name).strip())
        except (NotFound, AttributeError, UnicodeEncodeError):
            self.set_error(errors.SUBREDDIT_NOEXIST)
            return

        return sr

    def param_docs(self):
        return {
            self.param[0]: "the [fullname](#fullname) of a subreddit",
        }


class VCollection(Validator):
    def run(self, name):
        collection = Collection.by_name(name)
        if collection:
            return collection
        self.set_error(errors.COLLECTION_NOEXIST)


class VPromoTarget(Validator):
    default_param = ("targeting", "sr", "collection")

    def run(self, targeting, sr_name, collection_name):
        if targeting == "collection" and collection_name == "none":
            return Target(Frontpage.name)
        elif targeting == "none":
            return Target(Frontpage.name)
        elif targeting == "collection":
            collection = VCollection("collection").run(collection_name)
            if collection:
                return Target(collection)
            else:
                # VCollection added errors so no need to do anything
                return
        elif targeting == "one":
            sr = VSubmitSR("sr", promotion=True).run(sr_name)
            if sr:
                return Target(sr.name)
            else:
                # VSubmitSR added errors so no need to do anything
                return
        else:
            self.set_error(errors.INVALID_TARGET, field="targeting")


MIN_PASSWORD_LENGTH = 6

class VPassword(Validator):
    def run(self, password):
        if not (password and len(password) >= MIN_PASSWORD_LENGTH):
            self.set_error(errors.SHORT_PASSWORD, {"chars": MIN_PASSWORD_LENGTH})
            self.set_error(errors.BAD_PASSWORD)
        else:
            return password.encode('utf8')

    def param_docs(self):
        return {
            self.param[0]: "the password"
        }


class VPasswordChange(VPassword):
    def run(self, password, verify):
        base = super(VPasswordChange, self).run(password)

        if self.has_errors:
            return base

        if (verify != password):
            self.set_error(errors.BAD_PASSWORD_MATCH)
        else:
            return base

    def param_docs(self):
        return {
            self.param[0]: "the new password",
            self.param[1]: "the password again (for verification)",
        }

MIN_USERNAME_LENGTH = 3
MAX_USERNAME_LENGTH = 20

user_rx = re.compile(r"\A[\w-]+\Z", re.UNICODE)

def chkuser(x):
    if x is None:
        return None
    try:
        if any(ch.isspace() for ch in x):
            return None
        return str(x) if user_rx.match(x) else None
    except TypeError:
        return None
    except UnicodeEncodeError:
        return None

class VUname(VRequired):
    def __init__(self, item, *a, **kw):
        VRequired.__init__(self, item, errors.BAD_USERNAME, *a, **kw)
    def run(self, user_name):
        length = 0 if not user_name else len(user_name)
        if (length < MIN_USERNAME_LENGTH or length > MAX_USERNAME_LENGTH):
            msg_params = {
                'min': MIN_USERNAME_LENGTH,
                'max': MAX_USERNAME_LENGTH,
            }
            self.set_error(errors.USERNAME_TOO_SHORT, msg_params=msg_params)
            self.set_error(errors.BAD_USERNAME)
            return
        user_name = chkuser(user_name)
        if not user_name:
            self.set_error(errors.USERNAME_INVALID_CHARACTERS)
            self.set_error(errors.BAD_USERNAME)
            return
        else:
            try:
                a = Account._by_name(user_name, True)
                if a._deleted:
                   return self.set_error(errors.USERNAME_TAKEN_DEL)
                else:
                   return self.set_error(errors.USERNAME_TAKEN)
            except NotFound:
                return user_name

    def param_docs(self):
        return {
            self.param[0]: "a valid, unused, username",
        }

class VLoggedOut(Validator):
    def run(self):
        if c.user_is_loggedin:
            self.set_error(errors.LOGGED_IN)


class AuthenticationFailed(Exception):
    pass


class VThrottledLogin(VRequired):
    def __init__(self, params):
        VRequired.__init__(self, params, error=errors.WRONG_PASSWORD)
        self.vlength = VLength("user", max_length=100)
        self.seconds = None

    def run(self, username, password):
        if config["r2.import_private"]:
            from r2admin.lib.ip_events import ip_used_by_account
        else:
            def ip_used_by_account(account_id, ip):
                return False

        ratelimit_key = None

        try:
            if username:
                username = username.strip()
                username = self.vlength.run(username)
                username = chkuser(username)

            if not username:
                raise AuthenticationFailed

            try:
                account = Account._by_name(username)
            except NotFound:
                raise AuthenticationFailed

            hooks.get_hook("account.spotcheck").call(account=account)
            if account._banned:
                raise AuthenticationFailed

            # if already logged in, you're exempt from your own ratelimit
            # (e.g. to allow account deletion regardless of DoS)
            ratelimit_exempt = (account == c.user)
            if not ratelimit_exempt:
                time_slice = ratelimit.get_timeslice(g.RL_RESET_SECONDS)
                is_previously_seen_ip = ip_used_by_account(account._id, request.ip)
                if is_previously_seen_ip:
                    ratelimit_key = "rl-login-familiar-%d" % account._id
                else:
                    ratelimit_key = "rl-login-unknown-%d" % account._id

                try:
                    failed_logins = ratelimit.get_usage(ratelimit_key, time_slice)
                    if failed_logins >= g.RL_LOGIN_MAX_REQS:
                        self.seconds = time_slice.remaining
                        period_end = datetime.utcfromtimestamp(
                            time_slice.end).replace(tzinfo=pytz.UTC)
                        time = utils.timeuntil(period_end)
                        self.set_error(
                            errors.RATELIMIT, {'time': time},
                            field='ratelimit', code=429)
                        return False
                except ratelimit.RatelimitError as e:
                    g.log.info("ratelimitcache error (login): %s", e)

            try:
                str(password)
            except UnicodeEncodeError:
                password = password.encode("utf8")

            if not valid_password(account, password):
                raise AuthenticationFailed
            return account
        except AuthenticationFailed:
            if ratelimit_key:
                try:
                    ratelimit.record_usage(ratelimit_key, time_slice)
                except ratelimit.RatelimitError as e:
                    g.log.info("ratelimitcache error (login): %s", e)
            self.error()
            return False

    def param_docs(self):
        return {
            self.param[0]: "a username",
            self.param[1]: "the user's password",
        }


class VSanitizedUrl(Validator):
    def run(self, url):
        return utils.sanitize_url(url)

    def param_docs(self):
        return {self.param: "a valid URL"}


class VUrl(VRequired):
    def __init__(self, item, allow_self=True, require_scheme=False,
                 valid_schemes=utils.VALID_SCHEMES, *a, **kw):
        self.allow_self = allow_self
        self.require_scheme = require_scheme
        self.valid_schemes = valid_schemes
        VRequired.__init__(self, item, errors.NO_URL, *a, **kw)

    def run(self, url):
        if not url:
            return self.error(errors.NO_URL)

        url = utils.sanitize_url(url, require_scheme=self.require_scheme,
                                 valid_schemes=self.valid_schemes)
        if not url:
            return self.error(errors.BAD_URL)

        if url == 'self':
            if self.allow_self:
                return url
            else:
                self.error(errors.BAD_URL)
        else:
            return url

    def param_docs(self):
        return {self.param: "a valid URL"}


class VRedirectUri(VUrl):
    def __init__(self, item, valid_schemes=None, *a, **kw):
        VUrl.__init__(self, item, allow_self=False, require_scheme=True,
                      valid_schemes=valid_schemes, *a, **kw)

    def param_docs(self):
        doc = "a valid URI"
        if self.valid_schemes:
            doc += " with one of the following schemes: "
            doc += ", ".join(self.valid_schemes)
        return {self.param: doc}


class VShamedDomain(Validator):
    def run(self, url):
        if not url:
            return

        is_shamed, domain, reason = is_shamed_domain(url)

        if is_shamed:
            self.set_error(errors.DOMAIN_BANNED, dict(domain=domain,
                                                      reason=reason))

class VExistingUname(VRequired):
    def __init__(self, item, allow_deleted=False, *a, **kw):
        self.allow_deleted = allow_deleted
        VRequired.__init__(self, item, errors.NO_USER, *a, **kw)

    def run(self, name):
        if name and name.startswith('~') and c.user_is_admin:
            try:
                user_id = int(name[1:])
                return Account._byID(user_id, True)
            except (NotFound, ValueError):
                self.error(errors.USER_DOESNT_EXIST)

        # make sure the name satisfies our user name regexp before
        # bothering to look it up.
        name = chkuser(name)
        if name:
            try:
                return Account._by_name(name)
            except NotFound:
                if self.allow_deleted and c.user_is_admin:
                    try:
                        return Account._by_name(name, allow_deleted=True)
                    except NotFound:
                        pass

                self.error(errors.USER_DOESNT_EXIST)
        else:
            self.error()

    def param_docs(self):
        return {
            self.param: 'the name of an existing user'
        }

class VMessageRecipient(VExistingUname):
    def run(self, name):
        if not name:
            return self.error()
        is_subreddit = False
        if name.startswith('/r/'):
            name = name[3:]
            is_subreddit = True
        elif name.startswith('#'):
            name = name[1:]
            is_subreddit = True
        if is_subreddit:
            try:
                s = Subreddit._by_name(name)
                if isinstance(s, FakeSubreddit):
                    raise NotFound, "fake subreddit"
                if s._spam:
                    raise NotFound, "banned subreddit"
                return s
            except NotFound:
                self.set_error(errors.SUBREDDIT_NOEXIST)
        else:
            account = VExistingUname.run(self, name)
            if account and account._id in c.user.enemies:
                self.set_error(errors.USER_BLOCKED)
            else:
                return account

class VUserWithEmail(VExistingUname):
    def run(self, name):
        user = VExistingUname.run(self, name)
        if not user or not hasattr(user, 'email') or not user.email:
            return self.error(errors.NO_EMAIL_FOR_USER)
        return user


class VBoolean(Validator):
    def run(self, val):
        if val is True or val is False:
            # val is already a bool object, no processing needed
            return val
        lv = str(val).lower()
        if lv == 'off' or lv == '' or lv[0] in ("f", "n"):
            return False
        return bool(val)

    def param_docs(self):
        return {
            self.param: 'boolean value',
        }

class VNumber(Validator):
    def __init__(self, param, min=None, max=None, coerce = True,
                 error=errors.BAD_NUMBER, num_default=None,
                 *a, **kw):
        self.min = self.cast(min) if min is not None else None
        self.max = self.cast(max) if max is not None else None
        self.coerce = coerce
        self.error = error
        self.num_default = num_default
        Validator.__init__(self, param, *a, **kw)

    def cast(self, val):
        raise NotImplementedError

    def _set_error(self):
        if self.max is None and self.min is None:
            range = ""
        elif self.max is None:
            range = _("%(min)d to any") % dict(min=self.min)
        elif self.min is None:
            range = _("any to %(max)d") % dict(max=self.max)
        else:
            range = _("%(min)d to %(max)d") % dict(min=self.min, max=self.max)
        self.set_error(self.error, msg_params=dict(range=range))

    def run(self, val):
        if not val:
            return self.num_default
        try:
            val = self.cast(val)
            if self.min is not None and val < self.min:
                if self.coerce:
                    val = self.min
                else:
                    raise ValueError, ""
            elif self.max is not None and val > self.max:
                if self.coerce:
                    val = self.max
                else:
                    raise ValueError, ""
            return val
        except ValueError:
            self._set_error()

class VInt(VNumber):
    def cast(self, val):
        return int(val)

    def param_docs(self):
        if self.min is not None and self.max is not None:
            description = "an integer between %d and %d" % (self.min, self.max)
        elif self.min is not None:
            description = "an integer greater than %d" % self.min
        elif self.max is not None:
            description = "an integer less than %d" % self.max
        else:
            description = "an integer"

        if self.num_default is not None:
            description += " (default: %d)" % self.num_default

        return {self.param: description}

class VFloat(VNumber):
    def cast(self, val):
        return float(val)

class VCssName(Validator):
    """
    returns a name iff it consists of alphanumeric characters and
    possibly "-", and is below the length limit.
    """

    r_css_name = re.compile(r"\A[a-zA-Z0-9\-]{1,100}\Z")

    def run(self, name):
        if name:
            if self.r_css_name.match(name):
                return name
            else:
                self.set_error(errors.BAD_CSS_NAME)
        return ''

    def param_docs(self):
        return {
            self.param: "a valid subreddit image name",
        }


class VMenu(Validator):

    def __init__(self, param, menu_cls, remember = True, **kw):
        self.nav = menu_cls
        self.remember = remember
        param = (menu_cls.name, param)
        Validator.__init__(self, param, **kw)

    def run(self, sort, where):
        if self.remember:
            pref = "%s_%s" % (where, self.nav.name)
            user_prefs = copy(c.user.sort_options) if c.user else {}
            user_pref = user_prefs.get(pref)

            # check to see if a default param has been set
            if not sort:
                sort = user_pref

        # validate the sort
        if sort not in self.nav._options:
            sort = self.nav._default

        # commit the sort if changed and if this is a POST request
        if (self.remember and c.user_is_loggedin and sort != user_pref
            and request.method.upper() == 'POST'):
            user_prefs[pref] = sort
            c.user.sort_options = user_prefs
            user = c.user
            user._commit()

        return sort

    def param_docs(self):
        return {
            self.param[0]: 'one of (%s)' % ', '.join("`%s`" % s
                                                  for s in self.nav._options),
        }


class VRatelimit(Validator):
    def __init__(self, rate_user = False, rate_ip = False,
                 prefix = 'rate_', error = errors.RATELIMIT, *a, **kw):
        self.rate_user = rate_user
        self.rate_ip = rate_ip
        self.prefix = prefix
        self.error = error
        self.seconds = None
        Validator.__init__(self, *a, **kw)

    def run (self):
        from r2.models.admintools import admin_ratelimit

        if g.disable_ratelimit:
            return

        if c.user_is_loggedin and not admin_ratelimit(c.user):
            return

        to_check = []
        if self.rate_user and c.user_is_loggedin:
            to_check.append('user' + str(c.user._id36))
        if self.rate_ip:
            to_check.append('ip' + str(request.ip))

        r = g.cache.get_multi(to_check, self.prefix)
        if r:
            expire_time = max(r.values())
            time = utils.timeuntil(expire_time)

            g.log.debug("rate-limiting %s from %s" % (self.prefix, r.keys()))

            # when errors have associated field parameters, we'll need
            # to add that here
            if self.error == errors.RATELIMIT:
                from datetime import datetime
                delta = expire_time - datetime.now(g.tz)
                self.seconds = delta.total_seconds()
                if self.seconds < 3:  # Don't ratelimit within three seconds
                    return
                self.set_error(errors.RATELIMIT, {'time': time},
                               field='ratelimit', code=429)
            else:
                self.set_error(self.error)

    @classmethod
    def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_",
                  seconds = None):
        to_set = {}
        if seconds is None:
            seconds = g.RL_RESET_SECONDS
        expire_time = datetime.now(g.tz) + timedelta(seconds = seconds)
        if rate_user and c.user_is_loggedin:
            to_set['user' + str(c.user._id36)] = expire_time
        if rate_ip:
            to_set['ip' + str(request.ip)] = expire_time
        g.cache.set_multi(to_set, prefix = prefix, time = seconds)

class VDelay(Validator):
    def __init__(self, category, *a, **kw):
        self.category = category
        self.seconds = None
        Validator.__init__(self, *a, **kw)

    def run (self):
        if g.disable_ratelimit:
            return
        key = "VDelay-%s-%s" % (self.category, request.ip)
        prev_violations = g.cache.get(key)
        if prev_violations:
            time = utils.timeuntil(prev_violations["expire_time"])
            remaining = prev_violations["expire_time"] - datetime.now(g.tz)
            self.seconds = remaining.total_seconds()
            if self.seconds >= 3:
                self.set_error(errors.RATELIMIT, {'time': time},
                               field='vdelay', code=429)

    @classmethod
    def record_violation(self, category, seconds = None, growfast=False):
        if seconds is None:
            seconds = g.RL_RESET_SECONDS

        key = "VDelay-%s-%s" % (category, request.ip)
        prev_violations = g.cache.get(key)
        if prev_violations is None:
            prev_violations = dict(count=0)

        num_violations = prev_violations["count"]

        if growfast:
            multiplier = 3 ** num_violations
        else:
            multiplier = 1

        max_duration = 8 * 3600
        duration = min(seconds * multiplier, max_duration)

        expire_time = (datetime.now(g.tz) +
                       timedelta(seconds = duration))

        prev_violations["expire_time"] = expire_time
        prev_violations["duration"] = duration
        prev_violations["count"] += 1

        with g.make_lock("record_violation", "lock-" + key, timeout=5, verbose=False):
            existing = g.cache.get(key)
            if existing and existing["count"] > prev_violations["count"]:
                g.log.warning("Tried to set %s to count=%d, but found existing=%d"
                             % (key, prev_violations["count"], existing["count"]))
            else:
                g.cache.set(key, prev_violations, max_duration)

class VCommentIDs(Validator):
    def run(self, id_str):
        if id_str:
            try:
                cids = [int(i, 36) for i in id_str.split(',')]
                return cids
            except ValueError:
                abort(400)
        return []

    def param_docs(self):
        return {
            self.param: "a comma-delimited list of comment ID36s",
        }


class CachedUser(object):
    def __init__(self, cache_prefix, user, key):
        self.cache_prefix = cache_prefix
        self.user = user
        self.key = key

    def clear(self):
        if self.key and self.cache_prefix:
            g.cache.delete(str(self.cache_prefix + "_" + self.key))

class VOneTimeToken(Validator):
    def __init__(self, model, param, *args, **kwargs):
        self.model = model
        Validator.__init__(self, param, *args, **kwargs)

    def run(self, key):
        token = self.model.get_token(key)

        if token:
            return token
        else:
            self.set_error(errors.EXPIRED)
            return None

class VOneOf(Validator):
    def __init__(self, param, options = (), *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self.options = options

    def run(self, val):
        if self.options and val not in self.options:
            self.set_error(errors.INVALID_OPTION, code=400)
            return self.default
        else:
            return val

    def param_docs(self):
        return {
            self.param: 'one of (%s)' % ', '.join("`%s`" % s
                                                  for s in self.options),
        }


class VList(Validator):
    def __init__(self, param, separator=",", choices=None,
                 error=errors.INVALID_OPTION, *a, **kw):
        Validator.__init__(self, param, *a, **kw)
        self.separator = separator
        self.choices = choices
        self.error = error

    def run(self, items):
        if not items:
            return None
        all_values = items.split(self.separator)
        if self.choices is None:
            return all_values

        values = []
        for val in all_values:
            if val in self.choices:
                values.append(val)
            else:
                msg_params = {"choice": val}
                self.set_error(self.error, msg_params=msg_params,
                               code=400)
        return values

    # Not i18n'able, but param_docs are not currently i18n'ed
    NICE_SEP = {",": "comma"}
    def param_docs(self):
        if self.choices:
            msg = ("A %(separator)s-separated list of items from "
                   "this set:\n\n%(choices)s")
            choices = "`" + "`  \n`".join(self.choices) + "`"
        else:
            msg = "A %(separator)s-separated list of items"
            choices = None

        sep = self.NICE_SEP.get(self.separator, self.separator)
        docs = msg % {"separator": sep, "choices": choices}
        return {self.param: docs}


class VPriority(Validator):
    def run(self, val):
        if c.user_is_sponsor:
            return PROMOTE_PRIORITIES.get(val, PROMOTE_DEFAULT_PRIORITY)
        else:
            return PROMOTE_DEFAULT_PRIORITY


class VLocation(Validator):
    default_param = ("country", "region", "metro")

    def run(self, country, region, metro):
        # some browsers are sending "null" rather than omitting the input when
        # the select is disabled
        country, region, metro = map(lambda val: None if val == "null" else val,
                                     [country, region, metro])

        if not (country or region or metro):
            return None

        if not (country and not (region or metro) or
                (country and region and metro)):
            # can target just country or country, region, and metro
            self.set_error(errors.INVALID_LOCATION, code=400)
        elif (country not in g.locations or
              region and region not in g.locations[country]['regions'] or
              metro and metro not in g.locations[country]['regions'][region]['metros']):
            self.set_error(errors.INVALID_LOCATION, code=400)
        else:
            return Location(country, region, metro)


class VImageType(Validator):
    def run(self, img_type):
        if not img_type in ('png', 'jpg'):
            return 'png'
        return img_type

    def param_docs(self):
        return {
            self.param: "one of `png` or `jpg` (default: `png`)",
        }


class ValidEmail(Validator):
    """Validates a single email. Returns the email on success."""

    email_re  = re.compile(r'.+@.+\..+')

    def run(self, email):
        if not email:
            self.set_error(errors.NO_EMAIL)
        elif not self.email_re.match(email):
            self.set_error(errors.BAD_EMAIL)
        else:
            return email


class ValidEmails(Validator):
    """Validates a list of email addresses passed in as a string and
    delineated by whitespace, ',' or ';'.  Also validates quantity of
    provided emails.  Returns a list of valid email addresses on
    success"""

    separator = re.compile(r'[^\s,;]+')
    email_re  = re.compile(r'.+@.+\..+')

    def __init__(self, param, num = 20, **kw):
        self.num = num
        Validator.__init__(self, param = param, **kw)

    def run(self, emails0):
        emails = set(self.separator.findall(emails0) if emails0 else [])
        failures = set(e for e in emails if not self.email_re.match(e))
        emails = emails - failures

        # make sure the number of addresses does not exceed the max
        if self.num > 0 and len(emails) + len(failures) > self.num:
            # special case for 1: there should be no delineators at all, so
            # send back original string to the user
            if self.num == 1:
                self.set_error(errors.BAD_EMAILS,
                             {'emails': '"%s"' % emails0})
            # else report the number expected
            else:
                self.set_error(errors.TOO_MANY_EMAILS,
                             {'num': self.num})
        # correct number, but invalid formatting
        elif failures:
            self.set_error(errors.BAD_EMAILS,
                         {'emails': ', '.join(failures)})
        # no emails
        elif not emails:
            self.set_error(errors.NO_EMAILS)
        else:
            # return single email if one is expected, list otherwise
            return list(emails)[0] if self.num == 1 else emails

class ValidEmailsOrExistingUnames(Validator):
    """Validates a list of mixed email addresses and usernames passed in
    as a string, delineated by whitespace, ',' or ';'.  Validates total
    quantity too while we're at it.  Returns a tuple of the form
    (e-mail addresses, user account objects)"""

    def __init__(self, param, num=20, **kw):
        self.num = num
        Validator.__init__(self, param=param, **kw)

    def run(self, items):
        # Use ValidEmails separator to break the list up
        everything = set(ValidEmails.separator.findall(items) if items else [])

        # Use ValidEmails regex to divide the list into e-mail and other
        emails = set(e for e in everything if ValidEmails.email_re.match(e))
        failures = everything - emails

        # Run the rest of the validator against the e-mails list
        ve = ValidEmails(self.param, self.num)
        if len(emails) > 0:
            ve.run(", ".join(emails))

        # ValidEmails will add to c.errors for us, so do nothing if that fails
        # Elsewise, on with the users
        if not ve.has_errors:
            users = set()  # set of accounts
            validusers = set()  # set of usernames to subtract from failures

            # Now steal from VExistingUname:
            for uname in failures:
                check = uname
                if re.match('/u/', uname):
                    check = check[3:]
                veu = VExistingUname(check)
                account = veu.run(check)
                if account:
                    validusers.add(uname)
                    users.add(account)

            # We're fine if all our failures turned out to be valid users
            if len(users) == len(failures):
                # ValidEmails checked to see if there were too many addresses,
                # check to see if there's enough left-over space for users
                remaining = self.num - len(emails)
                if len(users) > remaining:
                    if self.num == 1:
                        # We only wanted one, and we got it as an e-mail,
                        # so complain.
                        self.set_error(errors.BAD_EMAILS,
                                       {"emails": '"%s"' % items})
                    else:
                        # Too many total
                        self.set_error(errors.TOO_MANY_EMAILS,
                                       {"num": self.num})
                elif len(users) + len(emails) == 0:
                    self.set_error(errors.NO_EMAILS)
                else:
                    # It's all good!
                    return (emails, users)
            else:
                failures = failures - validusers
                self.set_error(errors.BAD_EMAILS,
                               {'emails': ', '.join(failures)})

class VCnameDomain(Validator):
    domain_re  = re.compile(r'\A([\w\-_]+\.)+[\w]+\Z')

    def run(self, domain):
        if (domain
            and (not self.domain_re.match(domain)
                 or domain.endswith('.' + g.domain)
                 or domain.endswith('.' + g.media_domain)
                 or len(domain) > 300)):
            self.set_error(errors.BAD_CNAME)
        elif domain:
            try:
                return str(domain).lower()
            except UnicodeEncodeError:
                self.set_error(errors.BAD_CNAME)

    def param_docs(self):
        # cnames are dead; don't advertise this.
        return {}


class VDate(Validator):
    """
    Date checker that accepts string inputs.

    Optional parameters 'earliest' and 'latest' specify the acceptable timedelta
    offsets (offsets are inclusive).

    Error conditions:
       * BAD_DATE on mal-formed date strings (strptime parse failure)
       * DATE_TOO_EARLY and DATE_TOO_LATE on range errors.

    """
    def __init__(self, param, earliest=None, latest=None,
                 sponsor_override = False,
                 reference_date = lambda : datetime.now(g.tz),
                 business_days = False,
                 format = "%m/%d/%Y"):
        self.earliest = earliest
        self.latest = latest

        # are weekends to be exluded from the interval?
        self.business_days = business_days

        self.format = format

        # function for generating "now"
        self.reference_date = reference_date

        # do we let admins and sponsors override date range checking?
        self.override = sponsor_override
        Validator.__init__(self, param)

    def run(self, date):
        now = self.reference_date()
        earliest = latest = None
        if self.earliest:
            earliest = make_offset_date(now, self.earliest.days,
                                        business_days=self.business_days)
        if self.latest:
            latest = make_offset_date(now, self.latest.days,
                                      business_days=self.business_days)
        override = c.user_is_sponsor and self.override
        try:
            date = datetime.strptime(date, self.format)
            if not override:
                if earliest and not date.date() >= earliest.date():
                    self.set_error(errors.DATE_TOO_EARLY,
                                   {'day': earliest.strftime(self.format)})

                if latest and not date.date() <= latest.date():
                    self.set_error(errors.DATE_TOO_LATE,
                                   {'day': latest.strftime(self.format)})
            return date.replace(tzinfo=g.tz)
        except (ValueError, TypeError):
            self.set_error(errors.BAD_DATE)

class VDateRange(Validator):
    """
    Adds range validation to VDate.  In addition to satisfying
    future/past requirements in VDate, two date fields must be
    provided and they must be in order.

    If required is False, then the dates may be omitted without
    causing an error (but if a start date is provided, an end
    date MUST be provided as well).

    Additional Error conditions:
      * BAD_DATE_RANGE if start_date is not less than end_date
    """
    def __init__(self, param, max_range=None, required=True, **kw):
        self.max_range = max_range
        self.required = required
        self.vstart = VDate(param[0], **kw)
        self.vend = VDate(param[1], **kw)
        Validator.__init__(self, param)

    def run(self, start, end):
        try:
            start_date = self.vstart.run(start)
            end_date = self.vend.run(end)
            # If either date is missing and dates are "required",
            # it's a bad range. Additionally, if one date is missing,
            # but the other is provided, it's always an error.
            if not start_date or not end_date:
                if self.required or (not start_date and not end_date):
                    self.set_error(errors.BAD_DATE_RANGE)
                return (start_date, end_date)
            elif end_date < start_date:
                self.set_error(errors.BAD_DATE_RANGE)
            elif self.max_range and end_date - start_date > self.max_range:
                self.set_error(errors.DATE_RANGE_TOO_LARGE,
                               {'days': self.max_range})
            return (start_date, end_date)
        except ValueError:
            # insufficient number of arguments provided (expect 2)
            self.set_error(errors.BAD_DATE_RANGE)


class VDestination(Validator):
    def __init__(self, param = 'dest', default = "", **kw):
        Validator.__init__(self, param, default, **kw)

    def run(self, dest):
        if not dest:
            dest = self.default or add_sr("/")

        ld = dest.lower()
        if ld.startswith(('/', 'http://', 'https://')):
            u = UrlParser(dest)

            if u.is_reddit_url(c.site):
                return dest

        ip = getattr(request, "ip", "[unknown]")
        fp = getattr(request, "fullpath", "[unknown]")
        dm = c.domain or "[unknown]"
        cn = c.cname or "[unknown]"

        log_text("invalid redirect",
                 "%s attempted to redirect from %s to %s with domain %s and cname %s"
                      % (ip, fp, dest, dm, cn),
                 "info")

        return "/"

    def param_docs(self):
        return {
            self.param: 'destination url (must be same-domain)',
        }

class ValidAddress(Validator):
    def set_error(self, msg, field):
        Validator.set_error(self, errors.BAD_ADDRESS,
                            dict(message=msg), field = field)

    def run(self, firstName, lastName, company, address,
            city, state, zipCode, country, phoneNumber):
        if not firstName:
            self.set_error(_("please provide a first name"), "firstName")
        elif not lastName:
            self.set_error(_("please provide a last name"), "lastName")
        elif not address:
            self.set_error(_("please provide an address"), "address")
        elif not city:
            self.set_error(_("please provide your city"), "city")
        elif not state:
            self.set_error(_("please provide your state"), "state")
        elif not zipCode:
            self.set_error(_("please provide your zip or post code"), "zip")
        elif not country:
            self.set_error(_("please provide your country"), "country")

        # Make sure values don't exceed max length defined in the authorize.net
        # xml schema: https://api.authorize.net/xml/v1/schema/AnetApiSchema.xsd
        max_lengths = [
            (firstName, 50, 'firstName'), # (argument, max len, form field name)
            (lastName, 50, 'lastName'),
            (company, 50, 'company'),
            (address, 60, 'address'),
            (city, 40, 'city'),
            (state, 40, 'state'),
            (zipCode, 20, 'zip'),
            (country, 60, 'country'),
            (phoneNumber, 255, 'phoneNumber')
        ]
        for (arg, max_length, form_field_name) in max_lengths:
            if arg and len(arg) > max_length:
                self.set_error(_("max length %d characters" % max_length), form_field_name)

        if not self.has_errors:
            return Address(firstName = firstName,
                           lastName = lastName,
                           company = company or "",
                           address = address,
                           city = city, state = state,
                           zip = zipCode, country = country,
                           phoneNumber = phoneNumber or "")

class ValidCard(Validator):
    valid_date = re.compile(r"\d\d\d\d-\d\d")
    def set_error(self, msg, field):
        Validator.set_error(self, errors.BAD_CARD,
                            dict(message=msg), field = field)

    def run(self, cardNumber, expirationDate, cardCode):
        has_errors = False

        cardNumber = cardNumber or ""
        if not (cardNumber.isdigit() and 13 <= len(cardNumber) <= 16):
            self.set_error(_("credit card numbers should be 13 to 16 digits"),
                           "cardNumber")
            has_errors = True

        if not self.valid_date.match(expirationDate or ""):
            self.set_error(_("dates should be YYYY-MM"), "expirationDate")
            has_errors = True
        else:
            now = datetime.now(g.tz)
            yyyy, mm = expirationDate.split("-")
            year = int(yyyy)
            month = int(mm)
            if month < 1 or month > 12:
                self.set_error(_("month must be in the range 01..12"), "expirationDate")
                has_errors = True
            elif datetime(year, month, 1) < datetime(now.year, now.month, 1):
                self.set_error(_("expiration date must be in the future"), "expirationDate")
                has_errors = True

        cardCode = cardCode or ""
        if not (cardCode.isdigit() and 3 <= len(cardCode) <= 4):
            self.set_error(_("card verification codes should be 3 or 4 digits"),
                           "cardCode")
            has_errors = True

        if not has_errors:
            return CreditCard(cardNumber = cardNumber,
                              expirationDate = expirationDate,
                              cardCode = cardCode)

class VTarget(Validator):
    target_re = re.compile("\A[\w_-]{3,20}\Z")
    def run(self, name):
        if name and self.target_re.match(name):
            return name

    def param_docs(self):
        # this is just for htmllite and of no interest to api consumers
        return {}

class VFlairAccount(VRequired):
    def __init__(self, item, *a, **kw):
        VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)

    def _lookup(self, name, allow_deleted):
        try:
            return Account._by_name(name, allow_deleted=allow_deleted)
        except NotFound:
            return None

    def run(self, name):
        if not name:
            return self.error()
        return (
            self._lookup(name, False)
            or self._lookup(name, True)
            or self.error())

    def param_docs(self):
        return {self.param: _("a user by name")}

class VFlairLink(VRequired):
    def __init__(self, item, *a, **kw):
        VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)

    def run(self, name):
        if not name:
            return self.error()
        try:
            return Link._by_fullname(name, data=True)
        except NotFound:
            return self.error()

    def param_docs(self):
        return {self.param: _("a [fullname](#fullname) of a link")}

class VFlairCss(VCssName):
    def __init__(self, param, max_css_classes=10, **kw):
        self.max_css_classes = max_css_classes
        VCssName.__init__(self, param, **kw)

    def run(self, css):
        if not css:
            return css

        names = css.split()
        if len(names) > self.max_css_classes:
            self.set_error(errors.TOO_MUCH_FLAIR_CSS)
            return ''

        for name in names:
            if not self.r_css_name.match(name):
                self.set_error(errors.BAD_CSS_NAME)
                return ''

        return css

class VFlairText(VLength):
    def __init__(self, param, max_length=64, **kw):
        VLength.__init__(self, param, max_length, **kw)

class VFlairTemplateByID(VRequired):
    def __init__(self, param, **kw):
        VRequired.__init__(self, param, None, **kw)

    def run(self, flair_template_id):
        try:
            return FlairTemplateBySubredditIndex.get_template(
                c.site._id, flair_template_id)
        except tdb_cassandra.NotFound:
            return None

class VOneTimePassword(Validator):
    allowed_skew = [-1, 0, 1]  # allow a period of skew on either side of now
    ratelimit = 3  # maximum number of tries per period

    def __init__(self, param, required):
        self.required = required
        Validator.__init__(self, param)

    @classmethod
    def validate_otp(cls, secret, password):
        # is the password a valid format and has it been used?
        try:
            key = "otp-%s-%d" % (c.user._id36, int(password))
        except (TypeError, ValueError):
            valid_and_unused = False
        else:
            # leave this key around for one more time period than the maximum
            # number of time periods we'll check for valid passwords
            key_ttl = totp.PERIOD * (len(cls.allowed_skew) + 1)
            valid_and_unused = g.cache.add(key, True, time=key_ttl)

        # check the password (allowing for some clock-skew as 2FA-users
        # frequently travel at relativistic velocities)
        if valid_and_unused:
            for skew in cls.allowed_skew:
                expected_otp = totp.make_totp(secret, skew=skew)
                if constant_time_compare(password, expected_otp):
                    return True

        return False

    def run(self, password):
        # does the user have 2FA configured?
        secret = c.user.otp_secret
        if not secret:
            if self.required:
                self.set_error(errors.NO_OTP_SECRET)
            return

        # do they have the otp cookie instead?
        if c.otp_cached:
            return

        # make sure they're not trying this too much
        if not g.disable_ratelimit:
            current_password = totp.make_totp(secret)
            key = "otp-tries-" + current_password
            g.cache.add(key, 0)
            recent_attempts = g.cache.incr(key)
            if recent_attempts > self.ratelimit:
                self.set_error(errors.RATELIMIT, dict(time="30 seconds"))
                return

        # check the password
        if self.validate_otp(secret, password):
            return

        # if we got this far, their password was wrong, invalid or already used
        self.set_error(errors.WRONG_PASSWORD)

class VOAuth2ClientID(VRequired):
    default_param = "client_id"
    default_param_doc = _("an app")
    def __init__(self, param=None, *a, **kw):
        VRequired.__init__(self, param, errors.OAUTH2_INVALID_CLIENT, *a, **kw)

    def run(self, client_id):
        client_id = VRequired.run(self, client_id)
        if client_id:
            client = OAuth2Client.get_token(client_id)
            if client and not getattr(client, 'deleted', False):
                return client
            else:
                self.error()

    def param_docs(self):
        return {self.default_param: self.default_param_doc}

class VOAuth2ClientDeveloper(VOAuth2ClientID):
    default_param_doc = _("an app developed by the user")

    def run(self, client_id):
        client = super(VOAuth2ClientDeveloper, self).run(client_id)
        if not client or not client.has_developer(c.user):
            return self.error()
        return client

class VOAuth2Scope(VRequired):
    default_param = "scope"
    def __init__(self, param=None, *a, **kw):
        VRequired.__init__(self, param, errors.OAUTH2_INVALID_SCOPE, *a, **kw)

    def run(self, scope):
        scope = VRequired.run(self, scope)
        if scope:
            parsed_scope = OAuth2Scope(scope)
            if parsed_scope.is_valid():
                return parsed_scope
            else:
                self.error()

class VOAuth2RefreshToken(Validator):
    def __init__(self, param, *a, **kw):
        Validator.__init__(self, param, None, *a, **kw)

    def run(self, refresh_token_id):
        if refresh_token_id:
            try:
                token = OAuth2RefreshToken._byID(refresh_token_id)
            except tdb_cassandra.NotFound:
                self.set_error(errors.OAUTH2_INVALID_REFRESH_TOKEN)
                return None
            if not token.check_valid():
                self.set_error(errors.OAUTH2_INVALID_REFRESH_TOKEN)
                return None
            return token
        else:
            return None

class VPermissions(Validator):
    types = dict(
        moderator=ModeratorPermissionSet,
        moderator_invite=ModeratorPermissionSet,
    )

    def __init__(self, type_param, permissions_param, *a, **kw):
        Validator.__init__(self, (type_param, permissions_param), *a, **kw)

    def run(self, type, permissions):
        permission_class = self.types.get(type)
        if not permission_class:
            self.set_error(errors.INVALID_PERMISSION_TYPE, field=self.param[0])
            return (None, None)
        try:
            perm_set = permission_class.loads(permissions, validate=True)
        except ValueError:
            self.set_error(errors.INVALID_PERMISSIONS, field=self.param[1])
            return (None, None)
        return type, perm_set


class VJSON(Validator):
    def run(self, json_str):
        if not json_str:
            return self.set_error('JSON_PARSE_ERROR', code=400)
        else:
            try:
                return json.loads(json_str)
            except ValueError:
                return self.set_error('JSON_PARSE_ERROR', code=400)

    def param_docs(self):
        return {
            self.param: "JSON data",
        }


class VValidatedJSON(VJSON):
    """Apply validators to the values of JSON formatted data."""
    class ArrayOf(object):
        """A JSON array of objects with the specified schema."""
        def __init__(self, spec):
            self.spec = spec

        def run(self, data):
            if not isinstance(data, list):
                raise RedditError('JSON_INVALID', code=400)

            validated_data = []
            for item in data:
                validated_data.append(self.spec.run(item))
            return validated_data

        def spec_docs(self):
            spec_lines = []
            spec_lines.append('[')
            if hasattr(self.spec, 'spec_docs'):
                array_docs = self.spec.spec_docs()
            else:
                array_docs = self.spec.param_docs()[self.spec.param]
            for line in array_docs.split('\n'):
                spec_lines.append('  ' + line)
            spec_lines[-1] += ','
            spec_lines.append('  ...')
            spec_lines.append(']')
            return '\n'.join(spec_lines)


    class Object(object):
        """A JSON object with validators for specified fields."""
        def __init__(self, spec):
            self.spec = spec

        def run(self, data, ignore_missing=False):
            if not isinstance(data, dict):
                raise RedditError('JSON_INVALID', code=400)

            validated_data = {}
            for key, validator in self.spec.iteritems():
                try:
                    validated_data[key] = validator.run(data[key])
                except KeyError:
                    if ignore_missing:
                        continue
                    raise RedditError('JSON_MISSING_KEY', code=400,
                                      msg_params={'key': key})
            return validated_data

        def spec_docs(self):
            spec_docs = {}
            for key, validator in self.spec.iteritems():
                if hasattr(validator, 'spec_docs'):
                    spec_docs[key] = validator.spec_docs()
                elif hasattr(validator, 'param_docs'):
                    spec_docs.update(validator.param_docs())
                    if validator.docs:
                        spec_docs.update(validator.docs)

            # generate markdown json schema docs
            spec_lines = []
            spec_lines.append('{')
            for key in sorted(spec_docs.keys()):
                key_docs = spec_docs[key]
                # indent any new lines
                key_docs = key_docs.replace('\n', '\n  ')
                spec_lines.append('  "%s": %s,' % (key, key_docs))
            spec_lines.append('}')
            return '\n'.join(spec_lines)

    class PartialObject(Object):
        def run(self, data):
            super_ = super(VValidatedJSON.PartialObject, self)
            return super_.run(data, ignore_missing=True)

    def __init__(self, param, spec, **kw):
        VJSON.__init__(self, param, **kw)
        self.spec = spec

    def run(self, json_str):
        data = VJSON.run(self, json_str)
        if self.has_errors:
            return

        # Note: this relies on the fact that all validator errors are dumped
        # into a global (c.errors) and then checked by @validate.
        return self.spec.run(data)

    def docs_model(self):
        spec_md = self.spec.spec_docs()

        # indent for code formatting
        spec_md = '\n'.join(
            '    ' + line for line in spec_md.split('\n')
        )
        return spec_md

    def param_docs(self):
        return {
            self.param: 'json data:\n\n' + self.docs_model(),
        }


multi_name_rx = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{1,20}\Z")
multi_name_chars_rx = re.compile(r"[^A-Za-z0-9_]")

class VMultiPath(Validator):
    def __init__(self, param, kinds=None, **kw):
        Validator.__init__(self, param, **kw)
        self.kinds = tup(kinds or ('f', 'm'))

    @classmethod
    def normalize(self, path):
        if path[0] != '/':
            path = '/' + path
        path = path.lower().rstrip('/')
        return path

    def run(self, path):
        try:
            require(path)
            path = self.normalize(path)
            require(path.startswith('/user/'))
            user, username, kind, name = require_split(path, 5, sep='/')[1:]
            require(kind in self.kinds)
            username = chkuser(username)
            require(username)
        except RequirementException:
            self.set_error('BAD_MULTI_PATH', code=400)
            return

        try:
            require(multi_name_rx.match(name))
        except RequirementException:
            invalid_char = multi_name_chars_rx.search(name)
            if invalid_char:
                char = invalid_char.group()
                if char == ' ':
                    reason = _('no spaces allowed')
                else:
                    reason = _("invalid character: '%s'") % char
            elif name[0] == '_':
                reason = _("can't start with a '_'")
            elif len(name) < 2:
                reason = _('that name is too short')
            elif len(name) > 21:
                reason = _('that name is too long')
            else:
                reason = _("that name isn't going to work")

            self.set_error('BAD_MULTI_NAME', {'reason': reason}, code=400)
            return

        return {'path': path, 'username': username, 'name': name}

    def param_docs(self):
        return {
            self.param: "multireddit url path",
        }


class VMultiByPath(Validator):
    def __init__(self, param, require_view=True, require_edit=False, kinds=None):
        Validator.__init__(self, param)
        self.require_view = require_view
        self.require_edit = require_edit
        self.kinds = tup(kinds or ('f', 'm'))

    def run(self, path):
        path = VMultiPath.normalize(path)
        try:
            multi = LabeledMulti._byID(path)
        except tdb_cassandra.NotFound:
            return self.set_error('MULTI_NOT_FOUND', code=404)
        if not multi or multi.kind not in self.kinds:
            return self.set_error('MULTI_NOT_FOUND', code=404)
        if not multi or (self.require_view and not multi.can_view(c.user)):
            return self.set_error('MULTI_NOT_FOUND', code=404)
        if self.require_edit and not multi.can_edit(c.user):
            return self.set_error('MULTI_CANNOT_EDIT', code=403)

        return multi

    def param_docs(self):
        return {
            self.param: "multireddit url path",
        }
