Exemple #1
0
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {
            'unknown',
            'jquery-migrate-bad-html',
        }

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if 'msg' not in log or 'url' not in log:
                continue

            tag = 'unknown'

            if log.get('tag') in valid_frontend_log_tags:
                tag = log['tag']

            g.stats.simple_event('frontend.error.' + tag)

            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s',
                          level, log['msg'], uid, log['url'],
                          request.user_agent)

        VRatelimit.ratelimit(rate_user=False, rate_ip=True,
                             prefix="rate_weblog_", seconds=10)
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
Exemple #3
0
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {"unknown", "jquery-migrate-bad-html"}

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get("X-Loggit"):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else "-"

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if "msg" not in log or "url" not in log:
                continue

            tag = "unknown"

            if log.get("tag") in valid_frontend_log_tags:
                tag = log["tag"]

            g.stats.simple_event("frontend.error." + tag)

            g.log.warning(
                "[web frontend] %s: %s | U: %s FP: %s UA: %s", level, log["msg"], uid, log["url"], request.user_agent
            )

        VRatelimit.ratelimit(rate_user=False, rate_ip=True, prefix="rate_weblog_", seconds=10)
Exemple #4
0
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {
            'unknown',
            'jquery-migrate-bad-html',
        }

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if 'msg' not in log or 'url' not in log:
                continue

            tag = 'unknown'

            if log.get('tag') in valid_frontend_log_tags:
                tag = log['tag']

            g.stats.simple_event('frontend.error.' + tag)

            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level,
                          log['msg'], uid, log['url'], request.user_agent)

        VRatelimit.ratelimit(rate_user=False,
                             rate_ip=True,
                             prefix="rate_weblog_",
                             seconds=10)
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
Exemple #6
0
    def POST_message(self, level, logs):
        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s',
                          level, log['msg'], uid, log['url'],
                          request.user_agent)

        VRatelimit.ratelimit(rate_user=False, rate_ip=True,
                             prefix="rate_weblog_", seconds=10)
Exemple #7
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'),
        level=VOneOf('level', ('error',)),
        logs=VValidatedJSON('logs',
            VValidatedJSON.ArrayOf(VValidatedJSON.Object({
                'msg': VPrintable('msg', max_length=256),
                'url': VPrintable('url', max_length=256),
            }))
        ),
    )
    def POST_message(self, level, logs):
        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s',
                          level, log['msg'], uid, log['url'],
                          request.user_agent)

        VRatelimit.ratelimit(rate_user=False, rate_ip=True,
                             prefix="rate_weblog_", seconds=10)
Exemple #8
0
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(rate_user=True,
                             prefix="liveupdate_create_",
                             seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        amqp.add_item(
            "new_liveupdate_event",
            json.dumps({
                "event_fullname": event._fullname,
                "creator_fullname": c.user._fullname,
            }))

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
    def POST_create(self, form, jquery, title, description):
        if not is_event_configuration_valid(form):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            banned=c.user._spam,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
Exemple #10
0
class APIv1LoginController(RedditController):
    def pre(self):
        super(APIv1LoginController, self).pre()
        c.extension = "json"
        set_extension(request.environ, "json")

    @csrf_exempt
    @json_validate(
        VCaptcha(),
        VRatelimit(rate_ip=True, prefix="rate_register_"),
        signature=VSigned(),
        name=VUname(['user']),
        email=ValidEmail("email"),
        password=VPasswordChange(['passwd', 'passwd2']),
    )
    def POST_register(self, responder, name, email, password, **kwargs):
        kwargs.update(
            dict(
                controller=self,
                form=responder("noop"),
                responder=responder,
                name=name,
                email=email,
                password=password,
            ))
        return handle_register(**kwargs)

    @csrf_exempt
    @json_validate(
        signature=VSigned(),
        user=VThrottledLogin(['user', 'passwd']),
    )
    def POST_login(self, responder, user, **kwargs):
        kwargs.update(
            dict(
                controller=self,
                form=responder("noop"),
                responder=responder,
                user=user,
            ))
        return handle_login(**kwargs)

    def _login(self, responder, user, rem=None):
        """Login the user.

        AJAX login handler, used by both login and register to set the
        user cookie and send back a redirect.
        """
        c.user = user
        c.user_is_loggedin = True
        self.login(user, rem=rem)

        responder._send_data(modhash=generate_modhash())
        responder._send_data(cookie=user.make_cookie())
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
Exemple #12
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    @csrf_exempt
    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'),
        level=VOneOf('level', ('error', )),
        logs=VValidatedJSON(
            'logs',
            VValidatedJSON.ArrayOf(
                VValidatedJSON.PartialObject({
                    'msg':
                    VPrintable('msg', max_length=256),
                    'url':
                    VPrintable('url', max_length=256),
                    'tag':
                    VPrintable('tag', max_length=32),
                }))),
    )
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {
            'unknown',
            'jquery-migrate-bad-html',
        }

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if 'msg' not in log or 'url' not in log:
                continue

            tag = 'unknown'

            if log.get('tag') in valid_frontend_log_tags:
                tag = log['tag']

            g.stats.simple_event('frontend.error.' + tag)

            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level,
                          log['msg'], uid, log['url'], request.user_agent)

        VRatelimit.ratelimit(rate_user=False,
                             rate_ip=True,
                             prefix="rate_weblog_",
                             seconds=10)
    def POST_nominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        if form.has_errors("eligible", errors.DONATE_ACCOUNT_NOT_ELIGIBLE):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        else:
            VRatelimit.ratelimit(
                rate_user=True,
                prefix="donate_nominate_",
                seconds=NOMINATION_COOLDOWN,
            )

        DonationNominationsByAccount.nominate(
            c.user,
            organization,
        )
Exemple #14
0
    def POST_nominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        if form.has_errors("eligible", errors.DONATE_ACCOUNT_NOT_ELIGIBLE):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        else:
            VRatelimit.ratelimit(
                rate_user=True,
                prefix="donate_nominate_",
                seconds=NOMINATION_COOLDOWN,
            )

        DonationNominationsByAccount.nominate(
            c.user,
            organization,
        )
Exemple #15
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    @csrf_exempt
    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'),
        level=VOneOf('level', ('error', )),
        logs=VValidatedJSON(
            'logs',
            VValidatedJSON.ArrayOf(
                VValidatedJSON.PartialObject({
                    'msg':
                    VPrintable('msg', max_length=256),
                    'url':
                    VPrintable('url', max_length=256),
                    'tag':
                    VPrintable('tag', max_length=32),
                }))),
    )
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {
            'unknown',
            'jquery-migrate-bad-html',
        }

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if 'msg' not in log or 'url' not in log:
                continue

            tag = 'unknown'

            if log.get('tag') in valid_frontend_log_tags:
                tag = log['tag']

            g.stats.simple_event('frontend.error.' + tag)

            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level,
                          log['msg'], uid, log['url'], request.user_agent)

        VRatelimit.ratelimit(rate_user=False,
                             rate_ip=True,
                             prefix="rate_weblog_",
                             seconds=10)

    def OPTIONS_report_cache_poisoning(self):
        """Send CORS headers for cache poisoning reports."""
        if "Origin" not in request.headers:
            return
        origin = request.headers["Origin"]
        parsed_origin = UrlParser(origin)
        if not is_subdomain(parsed_origin.hostname, g.domain):
            return
        response.headers["Access-Control-Allow-Origin"] = origin
        response.headers["Access-Control-Allow-Methods"] = "POST"
        response.headers["Access-Control-Allow-Headers"] = \
            "Authorization, X-Loggit, "
        response.headers["Access-Control-Allow-Credentials"] = "false"
        response.headers['Access-Control-Expose-Headers'] = \
            self.COMMON_REDDIT_HEADERS

    @csrf_exempt
    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_poison_'),
        report_mac=VPrintable('report_mac', 255),
        poisoner_name=VPrintable('poisoner_name', 255),
        poisoner_id=VInt('poisoner_id'),
        poisoner_canary=VPrintable('poisoner_canary', 2, min_length=2),
        victim_canary=VPrintable('victim_canary', 2, min_length=2),
        render_time=VInt('render_time'),
        route_name=VPrintable('route_name', 255),
        url=VPrintable('url', 2048),
        # To differentiate between web and mweb in the future
        source=VOneOf('source', ('web', 'mweb')),
        cache_policy=VOneOf(
            'cache_policy',
            ('loggedin_www', 'loggedin_www_new', 'loggedin_mweb')),
        # JSON-encoded response headers from when our script re-requested
        # the poisoned page
        resp_headers=nop('resp_headers'),
    )
    def POST_report_cache_poisoning(
        self,
        report_mac,
        poisoner_name,
        poisoner_id,
        poisoner_canary,
        victim_canary,
        render_time,
        route_name,
        url,
        source,
        cache_policy,
        resp_headers,
    ):
        """Report an instance of cache poisoning and its details"""

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        # Eh? Why are you reporting this if the canaries are the same?
        if poisoner_canary == victim_canary:
            abort(400)

        expected_mac = make_poisoning_report_mac(
            poisoner_canary=poisoner_canary,
            poisoner_name=poisoner_name,
            poisoner_id=poisoner_id,
            cache_policy=cache_policy,
            source=source,
            route_name=route_name,
        )
        if not constant_time_compare(report_mac, expected_mac):
            abort(403)

        if resp_headers:
            try:
                resp_headers = json.loads(resp_headers)
                # Verify this is a JSON map of `header_name => [value, ...]`
                if not isinstance(resp_headers, dict):
                    abort(400)
                for hdr_name, hdr_vals in resp_headers.iteritems():
                    if not isinstance(hdr_name, basestring):
                        abort(400)
                    if not all(isinstance(h, basestring) for h in hdr_vals):
                        abort(400)
            except ValueError:
                abort(400)

        if not resp_headers:
            resp_headers = {}

        poison_info = dict(
            poisoner_name=poisoner_name,
            poisoner_id=str(poisoner_id),
            # Convert the JS timestamp to a standard one
            render_time=render_time * 1000,
            route_name=route_name,
            url=url,
            source=source,
            cache_policy=cache_policy,
            resp_headers=resp_headers,
        )

        # For immediate feedback when tracking the effects of caching changes
        g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy))
        # For longer-term diagnosing of caching issues
        g.events.cache_poisoning_event(poison_info, request=request, context=c)

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10)

        return self.api_wrapper({})
Exemple #16
0
    def POST_report_cache_poisoning(
        self,
        report_mac,
        poisoner_name,
        poisoner_id,
        poisoner_canary,
        victim_canary,
        render_time,
        route_name,
        url,
        source,
        cache_policy,
        resp_headers,
    ):
        """Report an instance of cache poisoning and its details"""

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        # Eh? Why are you reporting this if the canaries are the same?
        if poisoner_canary == victim_canary:
            abort(400)

        expected_mac = make_poisoning_report_mac(
            poisoner_canary=poisoner_canary,
            poisoner_name=poisoner_name,
            poisoner_id=poisoner_id,
            cache_policy=cache_policy,
            source=source,
            route_name=route_name,
        )
        if not constant_time_compare(report_mac, expected_mac):
            abort(403)

        if resp_headers:
            try:
                resp_headers = json.loads(resp_headers)
                # Verify this is a JSON map of `header_name => [value, ...]`
                if not isinstance(resp_headers, dict):
                    abort(400)
                for hdr_name, hdr_vals in resp_headers.iteritems():
                    if not isinstance(hdr_name, basestring):
                        abort(400)
                    if not all(isinstance(h, basestring) for h in hdr_vals):
                        abort(400)
            except ValueError:
                abort(400)

        if not resp_headers:
            resp_headers = {}

        poison_info = dict(
            poisoner_name=poisoner_name,
            poisoner_id=str(poisoner_id),
            # Convert the JS timestamp to a standard one
            render_time=render_time * 1000,
            route_name=route_name,
            url=url,
            source=source,
            cache_policy=cache_policy,
            resp_headers=resp_headers,
        )

        # For immediate feedback when tracking the effects of caching changes
        g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy))
        # For longer-term diagnosing of caching issues
        g.events.cache_poisoning_event(poison_info, request=request, context=c)

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10)

        return self.api_wrapper({})
Exemple #17
0
def handle_register(controller,
                    form,
                    responder,
                    name,
                    email,
                    password,
                    rem=None,
                    newsletter_subscribe=False,
                    sponsor=False,
                    signature=None,
                    **kwargs):
    def _event(error):
        g.events.login_event('register_attempt',
                             error_msg=error,
                             user_name=request.urlvars.get('url_user'),
                             email=request.POST.get('email'),
                             remember_me=rem,
                             newsletter=newsletter_subscribe,
                             signature=signature,
                             request=request,
                             context=c)

    if signature and not signature.is_valid():
        _event(error="SIGNATURE")
        abort(403)

    if responder.has_errors('user', errors.USERNAME_TOO_SHORT):
        _event(error='USERNAME_TOO_SHORT')

    elif responder.has_errors('user', errors.USERNAME_INVALID_CHARACTERS):
        _event(error='USERNAME_INVALID_CHARACTERS')

    elif responder.has_errors('user', errors.USERNAME_TAKEN_DEL):
        _event(error='USERNAME_TAKEN_DEL')

    elif responder.has_errors('user', errors.USERNAME_TAKEN):
        _event(error='USERNAME_TAKEN')

    elif responder.has_errors('email', errors.BAD_EMAIL):
        _event(error='BAD_EMAIL')

    elif responder.has_errors('passwd', errors.SHORT_PASSWORD):
        _event(error='SHORT_PASSWORD')

    elif responder.has_errors('passwd', errors.BAD_PASSWORD):
        # BAD_PASSWORD is set when SHORT_PASSWORD is set
        _event(error='BAD_PASSWORD')

    elif responder.has_errors('passwd2', errors.BAD_PASSWORD_MATCH):
        _event(error='BAD_PASSWORD_MATCH')

    elif responder.has_errors('ratelimit', errors.RATELIMIT):
        _event(error='RATELIMIT')

    elif (not g.disable_captcha
          and responder.has_errors('captcha', errors.BAD_CAPTCHA)):
        _event(error='BAD_CAPTCHA')

    #Blockpath may require email
    elif not email and g.config.get('verifyemailrequired', ()):
        c.errors.add(errors.NO_EMAIL, field="email")
        form.has_errors("email", errors.NO_EMAIL)
        _event(
            error='NO_EMAIL')  #might already be set by ValidEmail validator??

    elif newsletter_subscribe and not email:
        c.errors.add(errors.NEWSLETTER_NO_EMAIL, field="email")
        form.has_errors("email", errors.NEWSLETTER_NO_EMAIL)
        _event(error='NEWSLETTER_NO_EMAIL')

    elif sponsor and not email:
        c.errors.add(errors.SPONSOR_NO_EMAIL, field="email")
        form.has_errors("email", errors.SPONSOR_NO_EMAIL)
        _event(error='SPONSOR_NO_EMAIL')

    else:
        try:
            user = register(name, password, request.ip)
        except AccountExists:
            c.errors.add(errors.USERNAME_TAKEN, field="user")
            form.has_errors("user", errors.USERNAME_TAKEN)
            _event(error='USERNAME_TAKEN')
            return

        VRatelimit.ratelimit(rate_ip=True,
                             prefix="rate_register_",
                             seconds=86400)

        # anything else we know (email, languages)?
        if email:
            user.set_email(email)
            emailer.verify_email(user)

        user.pref_lang = c.lang
        user._commit()

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

        hooks.get_hook("account.registered").call(user=user)

        reject = hooks.get_hook("account.spotcheck").call(account=user)
        if any(reject):
            _event(error='ACCOUNT_SPOTCHECK')
            return

        if newsletter_subscribe and email:
            try:
                newsletter.add_subscriber(email, source="register")
            except newsletter.NewsletterError as e:
                g.log.warning("Failed to subscribe: %r" % e)

        controller._login(responder, user, rem)
        _event(error=None)
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, location, start, end):
        sr = sr or Frontpage
        if not location or not location.country:
            available = inventory.get_available_pageviews(sr,
                                                          start,
                                                          end,
                                                          datestr=True)
        else:
            available = inventory.get_available_pageviews_geotargeted(
                sr, location, start, end, datestr=True)
        return {'inventory': available}

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink("link_id36"),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_freebie(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        if campaign_has_oversold_error(form, campaign):
            form.set_html(".freebie", "target oversold, can't freebie")
            return

        if promote.is_promo(link) and campaign:
            promote.free_campaign(link, campaign, c.user)
            form.redirect(promote.promo_edit_url(link))

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VByName("link"),
                   note=nop("note"))
    def POST_promote_note(self, form, jquery, link, note):
        if promote.is_promo(link):
            text = PromotionLog.add(link, note)
            form.find(".notes").children(":last").after("<p>" + websafe(text) +
                                                        "</p>")

    @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id'))
    def POST_promote(self, thing):
        if promote.is_promo(thing):
            promote.accept_promotion(thing)

    @noresponse(VSponsorAdmin(),
                VModhash(),
                thing=VByName('id'),
                reason=nop("reason"))
    def POST_unpromote(self, thing, reason):
        if promote.is_promo(thing):
            promote.reject_promotion(thing, reason=reason)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link'),
                   campaign=VPromoCampaign('campaign'))
    def POST_refund_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign,
                                                      billable_impressions)
        refund_amount = promote.get_refund_amount(campaign, billable_amount)
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount,
                                    billable_impressions)
            form.set_html('.status', _('refund succeeded'))
        else:
            form.set_html('.status', _('refund not needed'))

    @validatedForm(VSponsor('link_id36'),
                   VModhash(),
                   VRatelimit(rate_user=True,
                              rate_ip=True,
                              prefix='create_promo_'),
                   VShamedDomain('url'),
                   username=VLength('username', 100, empty_error=None),
                   l=VLink('link_id36'),
                   title=VTitle('title'),
                   url=VUrl('url', allow_self=False),
                   selftext=VSelfText('text'),
                   kind=VOneOf('kind', ['link', 'self']),
                   ip=ValidIP(),
                   disable_comments=VBoolean("disable_comments"),
                   sendreplies=VBoolean("sendreplies"),
                   media_width=VInt("media-width", min=0),
                   media_height=VInt("media-height", min=0),
                   media_embed=VLength("media-embed", 1000),
                   media_override=VBoolean("media-override"),
                   domain_override=VLength("domain", 100))
    def POST_edit_promo(self, form, jquery, ip, username, l, title, url,
                        selftext, kind, disable_comments, sendreplies,
                        media_height, media_width, media_embed, media_override,
                        domain_override):

        should_ratelimit = False
        if not c.user_is_sponsor:
            should_ratelimit = True

        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        # check for user override
        if not l and c.user_is_sponsor and username:
            try:
                user = Account._by_name(username)
            except NotFound:
                c.errors.add(errors.USER_DOESNT_EXIST, field="username")
                form.set_error(errors.USER_DOESNT_EXIST, "username")
                return

            if not user.email:
                c.errors.add(errors.NO_EMAIL_FOR_USER, field="username")
                form.set_error(errors.NO_EMAIL_FOR_USER, "username")
                return

            if not user.email_verified:
                c.errors.add(errors.NO_VERIFIED_EMAIL, field="username")
                form.set_error(errors.NO_VERIFIED_EMAIL, "username")
                return
        else:
            user = c.user

        # check for shame banned domains
        if form.has_errors("url", errors.DOMAIN_BANNED):
            g.stats.simple_event('spam.shame.link')
            return

        # demangle URL in canonical way
        if url:
            if isinstance(url, (unicode, str)):
                form.set_inputs(url=url)
            elif isinstance(url, tuple) or isinstance(url[0], Link):
                # there's already one or more links with this URL, but
                # we're allowing mutliple submissions, so we really just
                # want the URL
                url = url[0].url

        if kind == 'link':
            if form.has_errors('url', errors.NO_URL, errors.BAD_URL):
                return

        # users can change the disable_comments on promoted links
        if ((not l or not promote.is_promoted(l))
                and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG)
                     or jquery.has_errors('ratelimit', errors.RATELIMIT))):
            return

        if not l:
            l = promote.new_promotion(title, url if kind == 'link' else 'self',
                                      selftext if kind == 'self' else '', user,
                                      ip)

        elif promote.is_promo(l):
            # changing link type is not allowed
            if ((l.is_self and kind == 'link')
                    or (not l.is_self and kind == 'self')):
                c.errors.add(errors.NO_CHANGE_KIND, field="kind")
                form.set_error(errors.NO_CHANGE_KIND, "kind")
                return

            changed = False
            # live items can only be changed by a sponsor, and also
            # pay the cost of de-approving the link
            trusted = c.user_is_sponsor or c.user.trusted_sponsor
            if not promote.is_promoted(l) or trusted:
                if title and title != l.title:
                    l.title = title
                    changed = not trusted

                if kind == 'link' and url and url != l.url:
                    l.url = url
                    changed = not trusted

            # only trips if the title and url are changed by a non-sponsor
            if changed:
                promote.unapprove_promotion(l)

            # selftext can be changed at any time
            if kind == 'self':
                l.selftext = selftext

            # comment disabling and sendreplies is free to be changed any time.
            l.disable_comments = disable_comments
            l.sendreplies = sendreplies
            if c.user_is_sponsor or c.user.trusted_sponsor:
                if media_embed and media_width and media_height:
                    l.media_object = dict(height=media_height,
                                          width=media_width,
                                          content=media_embed,
                                          type='custom')
                else:
                    l.media_object = None

                l.media_override = media_override
                if getattr(l, "domain_override", False) or domain_override:
                    l.domain_override = domain_override
            l._commit()

        form.redirect(promote.promo_edit_url(l))

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_add_roadblock(self, form, jquery, dates, sr):
        if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors(
                'enddate', errors.BAD_DATE, errors.BAD_DATE_RANGE)):
            return
        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.add(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_rm_roadblock(self, form, jquery, dates, sr):
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.remove(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsor('link_id36'),
                   VModhash(),
                   dates=VDateRange(
                       ['startdate', 'enddate'],
                       earliest=timedelta(days=g.min_promote_future),
                       latest=timedelta(days=g.max_promote_future),
                       reference_date=promote.promo_datetime_now,
                       business_days=True,
                       sponsor_override=True),
                   link=VLink('link_id36'),
                   bid=VFloat('bid', coerce=False),
                   sr=VSubmitSR('sr', promotion=True),
                   campaign_id36=nop("campaign_id36"),
                   targeting=VLength("targeting", 10),
                   priority=VPriority("priority"),
                   location=VLocation())
    def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid,
                           sr, targeting, priority, location):
        if not link:
            return

        start, end = dates or (None, None)

        if location and sr and not c.user_is_sponsor:
            # only sponsors can geotarget on subreddits
            location = None

        if location and location.metro:
            cpm = g.cpm_selfserve_geotarget_metro.pennies
        elif location:
            cpm = g.cpm_selfserve_geotarget_country.pennies
        else:
            author = Account._byID(link.author_id, data=True)
            cpm = author.cpm_selfserve_pennies

        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE)
                or form.has_errors('enddate', errors.BAD_DATE,
                                   errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE,
                                   errors.BAD_DATE_RANGE)):
            return

        # Limit the number of PromoCampaigns a Link can have
        # Note that the front end should prevent the user from getting
        # this far
        existing_campaigns = list(PromoCampaign._by_link(link._id))
        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:
            c.errors.add(errors.TOO_MANY_CAMPAIGNS,
                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},
                         field='title')
            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
            return

        campaign = None
        if campaign_id36:
            try:
                campaign = PromoCampaign._byID36(campaign_id36)
            except NotFound:
                pass

        if campaign and link._id != campaign.link_id:
            return abort(404, 'not found')

        if priority.cpm:
            min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
            max_bid = None if c.user_is_sponsor else g.max_promote_bid

            if bid is None or bid < min_bid or (max_bid and bid > max_bid):
                c.errors.add(errors.BAD_BID,
                             field='bid',
                             msg_params={
                                 'min': min_bid,
                                 'max': max_bid or g.max_promote_bid
                             })
                form.has_errors('bid', errors.BAD_BID)
                return

            # you cannot edit the bid of a live ad unless it's a freebie
            if (campaign and bid != campaign.bid
                    and promote.is_live_promo(link, campaign)
                    and not campaign.is_freebie()):
                c.errors.add(errors.BID_LIVE, field='bid')
                form.has_errors('bid', errors.BID_LIVE)
                return

        else:
            bid = 0.  # Set bid to 0 as dummy value

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

            roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end)
            if roadblock and not c.user_is_sponsor:
                msg_params = {
                    "start": roadblock[0].strftime('%m/%d/%Y'),
                    "end": roadblock[1].strftime('%m/%d/%Y')
                }
                c.errors.add(errors.OVERSOLD,
                             field='sr',
                             msg_params=msg_params)
                form.has_errors('sr', errors.OVERSOLD)
                return

        elif targeting == 'none':
            sr = None

        # Check inventory
        campaign = campaign if campaign_id36 else None
        if not priority.inventory_override:
            oversold = has_oversold_error(form, campaign, start, end, bid, cpm,
                                          sr, location)
            if oversold:
                return

        if campaign:
            promote.edit_campaign(link, campaign, dates, bid, cpm, sr,
                                  priority, location)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, sr,
                                            priority, location)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(VSponsor('link_id36'),
                   VModhash(),
                   l=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_delete_campaign(self, form, jquery, l, campaign):
        if not campaign or not l or l._id != campaign.link_id:
            return abort(404, 'not found')

        promote.delete_campaign(l, campaign)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_terminate_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        promote.terminate_campaign(link, campaign)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(VSponsor('link'),
                   VModhash(),
                   link=VByName("link"),
                   campaign=VPromoCampaign("campaign"),
                   customer_id=VInt("customer_id", min=0),
                   pay_id=VInt("account", min=0),
                   edit=VBoolean("edit"),
                   address=ValidAddress([
                       "firstName", "lastName", "company", "address", "city",
                       "state", "zip", "country", "phoneNumber"
                   ]),
                   creditcard=ValidCard(
                       ["cardNumber", "expirationDate", "cardCode"]))
    def POST_update_pay(self, form, jquery, link, campaign, customer_id,
                        pay_id, edit, address, creditcard):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        # Check inventory
        if campaign_has_oversold_error(form, campaign):
            return

        address_modified = not pay_id or edit
        form_has_errors = False
        if address_modified:
            if (form.has_errors([
                    "firstName", "lastName", "company", "address", "city",
                    "state", "zip", "country", "phoneNumber"
            ], errors.BAD_ADDRESS) or form.has_errors(
                ["cardNumber", "expirationDate", "cardCode"],
                    errors.BAD_CARD)):
                form_has_errors = True
            elif g.authorizenetapi:
                pay_id = edit_profile(c.user, address, creditcard, pay_id)
            else:
                pay_id = 1
        # if link is in use or finished, don't make a change
        if pay_id and not form_has_errors:
            # valid bid and created or existing bid id.
            # check if already a transaction
            if g.authorizenetapi:
                success, reason = promote.auth_campaign(
                    link, campaign, c.user, pay_id)
            else:
                success = True
            if success:
                form.redirect(promote.promo_edit_url(link))
            else:
                form.set_html(
                    ".status", reason
                    or _("failed to authenticate card.  sorry."))

    @validate(VSponsor("link_name"),
              VModhash(),
              link=VByName('link_name'),
              file=VUploadLength('file', 500 * 1024),
              img_type=VImageType('img_type'))
    def POST_link_thumb(self, link=None, file=None, img_type='jpg'):
        if link and (not promote.is_promoted(link) or c.user_is_sponsor
                     or c.user.trusted_sponsor):
            errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="")
            try:
                # thumnails for promoted links can change and therefore expire
                force_thumbnail(link, file, file_type=".%s" % img_type)
            except cssfilter.BadImage:
                # if the image doesn't clean up nicely, abort
                errors["IMAGE_ERROR"] = _("bad image")
            if any(errors.values()):
                return UploadedImage("",
                                     "",
                                     "upload",
                                     errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'),
                                     thumbnail_url(link),
                                     "",
                                     errors=errors,
                                     form_id="image-upload").render()
class LiveUpdateEventsController(RedditController):
    def GET_home(self):
        return pages.LiveUpdateMetaPage(
            title=_("reddit live"),
            content=pages.LiveUpdateHome(),
            page_classes=["liveupdate-home"],
        ).render()

    @validate(
        VEmployee(),
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateEvent("after"),
        before=VLiveUpdateEvent("before"),
        count=VCount("count"),
    )
    def GET_listing(self, filter, num, after, before, count):
        reverse = False
        if before:
            after = before
            reverse = True

        builder_cls = LiveUpdateEventBuilder
        wrapper = Wrapped
        listing_cls = Listing

        if filter == "open":
            title = _("live threads")
            query = queries.get_live_events("new", "all")
        elif filter == "closed":
            title = _("closed threads")
            query = queries.get_complete_events("new", "all")
        elif filter == "active":
            title = _("most active threads")
            query = queries.get_active_events()
        elif filter == "reported":
            if not c.user_is_admin:
                self.abort403()

            title = _("reported threads")
            query = queries.get_reported_events()
            builder_cls = LiveUpdateReportedEventBuilder
            wrapper = pages.LiveUpdateReportedEventRow
            listing_cls = pages.LiveUpdateReportedEventListing
        else:
            self.abort404()

        builder = builder_cls(
            query,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
            wrap=wrapper,
            skip=True,
        )

        listing = listing_cls(builder)

        return pages.LiveUpdateMetaPage(
            title=title,
            content=listing.listing(),
        ).render()

    @validate(
        VUser(),
    )
    def GET_create(self):
        return pages.LiveUpdateMetaPage(
            title=_("create live thread"),
            content=pages.LiveUpdateCreate(),
        ).render()

    @require_oauth2_scope("submit")
    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="liveupdate_create_"),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
        uri="/api/live/create",
    )
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
Exemple #20
0
    def POST_report_cache_poisoning(
            self,
            report_mac,
            poisoner_name,
            poisoner_id,
            poisoner_canary,
            victim_canary,
            render_time,
            route_name,
            url,
            source,
            cache_policy,
            resp_headers,
    ):
        """Report an instance of cache poisoning and its details"""

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        # Eh? Why are you reporting this if the canaries are the same?
        if poisoner_canary == victim_canary:
            abort(400)

        expected_mac = make_poisoning_report_mac(
            poisoner_canary=poisoner_canary,
            poisoner_name=poisoner_name,
            poisoner_id=poisoner_id,
            cache_policy=cache_policy,
            source=source,
            route_name=route_name,
        )
        if not constant_time_compare(report_mac, expected_mac):
            abort(403)

        if resp_headers:
            try:
                resp_headers = json.loads(resp_headers)
                # Verify this is a JSON map of `header_name => [value, ...]`
                if not isinstance(resp_headers, dict):
                    abort(400)
                for hdr_name, hdr_vals in resp_headers.iteritems():
                    if not isinstance(hdr_name, basestring):
                        abort(400)
                    if not all(isinstance(h, basestring) for h in hdr_vals):
                        abort(400)
            except ValueError:
                abort(400)

        if not resp_headers:
            resp_headers = {}

        poison_info = dict(
            poisoner_name=poisoner_name,
            poisoner_id=str(poisoner_id),
            # Convert the JS timestamp to a standard one
            render_time=render_time * 1000,
            route_name=route_name,
            url=url,
            source=source,
            cache_policy=cache_policy,
            resp_headers=resp_headers,
        )

        # For immediate feedback when tracking the effects of caching changes
        g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy))
        # For longer-term diagnosing of caching issues
        g.events.cache_poisoning_event(poison_info, request=request, context=c)

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10)

        return self.api_wrapper({})
Exemple #21
0
class DonateController(RedditController):
    def GET_closed(self):
        return pages.DonatePage(
            title=_("reddit donate"),
            content=pages.DonateClosed(),
        ).render()

    @validate(
        eligible=VAccountEligible(),
        organization=VOrganization("organization"),
    )
    def GET_landing(self, eligible, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if c.user_is_loggedin:
            nomination_count = DonationNominationsByAccount.count(c.user)
        else:
            nomination_count = None

        if organization:
            wrapped_organization = inject_nomination_status([organization])
        else:
            wrapped_organization = None

        content = pages.DonateLanding(eligible=eligible, )

        og_data = {
            "site_name": "reddit.com",
        }

        if organization:
            og_data[
                "title"] = "reddit donate: vote for %s" % organization.data[
                    "DisplayName"]
            og_data["url"] = join_urls(
                g.origin, "donate?organization=%s" % organization.data["EIN"])
        else:
            og_data["title"] = "reddit donate: giving 10% back"
            og_data["url"] = join_urls(g.origin, "donate")

        return pages.DonatePage(
            title=_("reddit donate"),
            content=content,
            og_data=og_data,
            extra_js_config={
                "unloadedNominations": nomination_count,
                "accountIsEligible": eligible,
                "organization": wrapped_organization,
            },
        ).render()

    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="donate_nominate_"),
        VAccountEligible(),
        organization=VOrganization("organization"),
    )
    def POST_nominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        if form.has_errors("eligible", errors.DONATE_ACCOUNT_NOT_ELIGIBLE):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        else:
            VRatelimit.ratelimit(
                rate_user=True,
                prefix="donate_nominate_",
                seconds=NOMINATION_COOLDOWN,
            )

        DonationNominationsByAccount.nominate(
            c.user,
            organization,
        )

    @validatedForm(
        VUser(),
        VModhash(),
        organization=VOrganization("organization"),
    )
    def POST_unnominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        DonationNominationsByAccount.unnominate(
            c.user,
            organization,
        )

    @json_validate(
        organization=VOrganization("organization"), )
    def GET_organization(self, responder, organization):
        """Look up a single org by EIN."""

        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if responder.has_errors("organization",
                                errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        wrapped = inject_nomination_status([organization])
        return wrapped[0]

    @json_validate(
        prefix=VLength("prefix", min_length=3, max_length=100), )
    def GET_search(self, responder, prefix):
        """Get organizations by display-name prefix."""

        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if responder.has_errors("prefix", errors.TOO_LONG, errors.TOO_SHORT):
            return

        organizations = DonationOrganizationsByPrefix.byPrefix(prefix)
        return inject_nomination_status(organizations)

    @json_validate(
        VUser(), )
    def GET_nominations(self, responder):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()
        nominated_org_ids = DonationNominationsByAccount.get_for(c.user)
        orgs = DonationOrganization.byEIN(nominated_org_ids)
        wrapped = inject_nomination_status(orgs, assume_nominated=True)
        return wrapped
Exemple #22
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    @csrf_exempt
    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'),
        level=VOneOf('level', ('error', )),
        logs=VValidatedJSON(
            'logs',
            VValidatedJSON.ArrayOf(
                VValidatedJSON.PartialObject({
                    'msg':
                    VPrintable('msg', max_length=256),
                    'url':
                    VPrintable('url', max_length=256),
                    'tag':
                    VPrintable('tag', max_length=32),
                }))),
    )
    def POST_message(self, level, logs):
        # Whitelist tags to keep the frontend from creating too many keys in statsd
        valid_frontend_log_tags = {
            'unknown',
            'jquery-migrate-bad-html',
        }

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        uid = c.user._id if c.user_is_loggedin else '-'

        # only accept a maximum of 3 entries per request
        for log in logs[:3]:
            if 'msg' not in log or 'url' not in log:
                continue

            tag = 'unknown'

            if log.get('tag') in valid_frontend_log_tags:
                tag = log['tag']

            g.stats.simple_event('frontend.error.' + tag)

            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level,
                          log['msg'], uid, log['url'], request.user_agent)

        VRatelimit.ratelimit(rate_user=False,
                             rate_ip=True,
                             prefix="rate_weblog_",
                             seconds=10)

    @csrf_exempt
    @validate(
        # set default to invalid number so we can ignore it later.
        dns_timing=VFloat('dnsTiming', min=0, num_default=-1),
        tcp_timing=VFloat('tcpTiming', min=0, num_default=-1),
        request_timing=VFloat('requestTiming', min=0, num_default=-1),
        response_timing=VFloat('responseTiming', min=0, num_default=-1),
        dom_loading_timing=VFloat('domLoadingTiming', min=0, num_default=-1),
        dom_interactive_timing=VFloat('domInteractiveTiming',
                                      min=0,
                                      num_default=-1),
        dom_content_loaded_timing=VFloat('domContentLoadedTiming',
                                         min=0,
                                         num_default=-1),
        action_name=VPrintable('actionName', max_length=256),
        verification=VPrintable('verification', max_length=256),
    )
    def POST_timings(self, action_name, verification, **kwargs):
        lookup = {
            'dns_timing': 'dns',
            'tcp_timing': 'tcp',
            'request_timing': 'request',
            'response_timing': 'response',
            'dom_loading_timing': 'dom_loading',
            'dom_interactive_timing': 'dom_interactive',
            'dom_content_loaded_timing': 'dom_content_loaded',
        }

        if not (action_name and verification):
            abort(422)

        expected_mac = hmac.new(g.secrets["action_name"], action_name,
                                hashlib.sha1).hexdigest()

        if not constant_time_compare(verification, expected_mac):
            abort(422)

        # action_name comes in the format 'controller.METHOD_action'
        stat_tpl = 'service_time.web.{}.frontend'.format(action_name)
        stat_aggregate = 'service_time.web.frontend'

        for key, name in lookup.iteritems():
            val = kwargs[key]
            if val >= 0:
                g.stats.simple_timing(stat_tpl + '.' + name, val)
                g.stats.simple_timing(stat_aggregate + '.' + name, val)

        abort(204)
Exemple #23
0
def handle_register(
    controller, form, responder, name, email,
    password, rem=None, newsletter_subscribe=False,
    sponsor=False, signature=None, **kwargs
):

    def _event(error):
        g.events.login_event(
            'register_attempt',
            error_msg=error,
            user_name=request.urlvars.get('url_user'),
            email=request.POST.get('email'),
            remember_me=rem,
            newsletter=newsletter_subscribe,
            signature=signature,
            request=request,
            context=c)

    if signature and not signature.is_valid():
        _event(error="SIGNATURE")
        abort(403)

    if responder.has_errors('user', errors.USERNAME_TOO_SHORT):
        _event(error='USERNAME_TOO_SHORT')

    elif responder.has_errors('user', errors.USERNAME_INVALID_CHARACTERS):
        _event(error='USERNAME_INVALID_CHARACTERS')

    elif responder.has_errors('user', errors.USERNAME_TAKEN_DEL):
        _event(error='USERNAME_TAKEN_DEL')

    elif responder.has_errors('user', errors.USERNAME_TAKEN):
        _event(error='USERNAME_TAKEN')

    elif responder.has_errors('email', errors.BAD_EMAIL):
        _event(error='BAD_EMAIL')

    elif responder.has_errors('passwd', errors.SHORT_PASSWORD):
        _event(error='SHORT_PASSWORD')

    elif responder.has_errors('passwd', errors.BAD_PASSWORD):
        # BAD_PASSWORD is set when SHORT_PASSWORD is set
        _event(error='BAD_PASSWORD')

    elif responder.has_errors('passwd2', errors.BAD_PASSWORD_MATCH):
        _event(error='BAD_PASSWORD_MATCH')

    elif responder.has_errors('ratelimit', errors.RATELIMIT):
        _event(error='RATELIMIT')

    elif (not g.disable_captcha and
            responder.has_errors('captcha', errors.BAD_CAPTCHA)):
        _event(error='BAD_CAPTCHA')

    elif newsletter_subscribe and not email:
        c.errors.add(errors.NEWSLETTER_NO_EMAIL, field="email")
        form.has_errors("email", errors.NEWSLETTER_NO_EMAIL)
        _event(error='NEWSLETTER_NO_EMAIL')

    elif sponsor and not email:
        c.errors.add(errors.SPONSOR_NO_EMAIL, field="email")
        form.has_errors("email", errors.SPONSOR_NO_EMAIL)
        _event(error='SPONSOR_NO_EMAIL')

    else:
        try:
            user = register(name, password, request.ip)
        except AccountExists:
            c.errors.add(errors.USERNAME_TAKEN, field="user")
            form.has_errors("user", errors.USERNAME_TAKEN)
            _event(error='USERNAME_TAKEN')
            return

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_register_")

        # anything else we know (email, languages)?
        if email:
            user.set_email(email)
            emailer.verify_email(user)

        user.pref_lang = c.lang
        user._commit()

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

        hooks.get_hook("account.registered").call(user=user)

        reject = hooks.get_hook("account.spotcheck").call(account=user)
        if any(reject):
            _event(error='ACCOUNT_SPOTCHECK')
            return

        if newsletter_subscribe and email:
            try:
                newsletter.add_subscriber(email, source="register")
            except newsletter.NewsletterError as e:
                g.log.warning("Failed to subscribe: %r" % e)

        controller._login(responder, user, rem)
        _event(error=None)
Exemple #24
0
def handle_register(
    controller, form, responder, name, email,
    password, rem=None, newsletter_subscribe=False,
    sponsor=False, signature=None, **kwargs
):
    # check captcha before register (if any) since its answer might
    # change once c.user is set.
    captcha_shown = not signature and need_provider_captcha("register")

    def _event(error, captcha_shown=captcha_shown):
        g.events.login_event(
            'register_attempt',
            error_msg=error,
            user_name=request.urlvars.get('url_user'),
            email=request.POST.get('email'),
            remember_me=rem,
            newsletter=newsletter_subscribe,
            captcha_shown=captcha_shown,
            signature=signature,
            request=request,
            context=c)

    if signature and not signature.is_valid():
        _event(error="SIGNATURE")
        abort(403)

    if responder.has_errors('user', errors.USERNAME_TOO_SHORT):
        _event(error='USERNAME_TOO_SHORT')

    elif responder.has_errors('user', errors.USERNAME_INVALID_CHARACTERS):
        _event(error='USERNAME_INVALID_CHARACTERS')

    elif responder.has_errors('user', errors.USERNAME_TAKEN_DEL):
        _event(error='USERNAME_TAKEN_DEL')

    elif responder.has_errors('user', errors.USERNAME_TAKEN):
        _event(error='USERNAME_TAKEN')

    elif responder.has_errors('email', errors.BAD_EMAIL):
        _event(error='BAD_EMAIL')

    elif responder.has_errors('passwd', errors.SHORT_PASSWORD):
        _event(error='SHORT_PASSWORD')

    elif responder.has_errors('passwd', errors.BAD_PASSWORD):
        # BAD_PASSWORD is set when SHORT_PASSWORD is set
        _event(error='BAD_PASSWORD')

    elif responder.has_errors('passwd2', errors.BAD_PASSWORD_MATCH):
        _event(error='BAD_PASSWORD_MATCH')

    elif responder.has_errors('ratelimit', errors.RATELIMIT):
        _event(error='RATELIMIT')

    elif newsletter_subscribe and not email:
        c.errors.add(errors.NEWSLETTER_NO_EMAIL, field="email")
        form.has_errors("email", errors.NEWSLETTER_NO_EMAIL)
        _event(error='NEWSLETTER_NO_EMAIL')

    elif sponsor and not email:
        c.errors.add(errors.SPONSOR_NO_EMAIL, field="email")
        form.has_errors("email", errors.SPONSOR_NO_EMAIL)
        _event(error='SPONSOR_NO_EMAIL')

    # last but not least, we have to check the captcha
    elif (not signature and not g.disable_captcha and
          not valid_provider_captcha(responder, "register")):
        _event(error='BAD_CAPTCHA')

    else:
        try:
            user = register(name, password, request.ip)
        except AccountExists:
            c.errors.add(errors.USERNAME_TAKEN, field="user")
            form.has_errors("user", errors.USERNAME_TAKEN)
            _event(error='USERNAME_TAKEN')
            return

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_register_")

        # anything else we know (email, languages)?
        if email:
            user.set_email(email)
            emailer.verify_email(user)

        user.pref_lang = c.lang

        if (is_api("html") and
                feature.is_enabled("new_user_onboarding", user=user)):
            user.has_been_onboarded = False

        user._commit()

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

        hooks.get_hook("account.registered").call(user=user)

        reject = hooks.get_hook("account.spotcheck").call(account=user)
        if any(reject):
            _event(error='ACCOUNT_SPOTCHECK')
            return

        if newsletter_subscribe and email:
            try:
                newsletter.add_subscriber(email, source="register")
            except newsletter.NewsletterError as e:
                g.log.warning("Failed to subscribe: %r" % e)

        controller._login(responder, user, rem)
        _event(error=None)
Exemple #25
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   collection=VCollection('collection'),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, collection, location, start,
                            end):
        if collection:
            target = Target(collection)
            sr = None
        else:
            sr = sr or Frontpage
            target = Target(sr.name)

        if not allowed_location_and_target(location, target):
            return abort(403, 'forbidden')

        available = inventory.get_available_pageviews(target,
                                                      start,
                                                      end,
                                                      location=location,
                                                      datestr=True)

        return {'inventory': available}

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink("link_id36"),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_freebie(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        if campaign_has_oversold_error(form, campaign):
            form.set_text(".freebie", _("target oversold, can't freebie"))
            return

        if promote.is_promo(link) and campaign:
            promote.free_campaign(link, campaign, c.user)
            form.redirect(promote.promo_edit_url(link))

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VByName("link"),
                   note=nop("note"))
    def POST_promote_note(self, form, jquery, link, note):
        if promote.is_promo(link):
            text = PromotionLog.add(link, note)
            form.find(".notes").children(":last").after("<p>" + websafe(text) +
                                                        "</p>")

    @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id'))
    def POST_promote(self, thing):
        if promote.is_promo(thing):
            promote.accept_promotion(thing)

    @noresponse(VSponsorAdmin(),
                VModhash(),
                thing=VByName('id'),
                reason=nop("reason"))
    def POST_unpromote(self, thing, reason):
        if promote.is_promo(thing):
            promote.reject_promotion(thing, reason=reason)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link'),
                   campaign=VPromoCampaign('campaign'))
    def POST_refund_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign,
                                                      billable_impressions)
        refund_amount = promote.get_refund_amount(campaign, billable_amount)
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount,
                                    billable_impressions)
            form.set_text('.status', _('refund succeeded'))
        else:
            form.set_text('.status', _('refund not needed'))

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        l=VLink('link_id36'),
        title=VTitle('title'),
        url=VUrl('url', allow_self=False),
        selftext=VMarkdownLength('text', max_length=40000),
        kind=VOneOf('kind', ['link', 'self']),
        disable_comments=VBoolean("disable_comments"),
        sendreplies=VBoolean("sendreplies"),
        media_url=VUrl("media_url",
                       allow_self=False,
                       valid_schemes=('http', 'https')),
        gifts_embed_url=VUrl("gifts_embed_url",
                             allow_self=False,
                             valid_schemes=('http', 'https')),
        media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")),
        media_autoplay=VBoolean("media_autoplay"),
        media_override=VBoolean("media-override"),
        domain_override=VLength("domain", 100),
        is_managed=VBoolean("is_managed"),
    )
    def POST_edit_promo(self, form, jquery, username, l, title, url, selftext,
                        kind, disable_comments, sendreplies, media_url,
                        media_autoplay, media_override, gifts_embed_url,
                        media_url_type, domain_override, is_managed):

        should_ratelimit = False
        if not c.user_is_sponsor:
            should_ratelimit = True

        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        # check for user override
        if not l and c.user_is_sponsor and username:
            try:
                user = Account._by_name(username)
            except NotFound:
                c.errors.add(errors.USER_DOESNT_EXIST, field="username")
                form.set_error(errors.USER_DOESNT_EXIST, "username")
                return

            if not user.email:
                c.errors.add(errors.NO_EMAIL_FOR_USER, field="username")
                form.set_error(errors.NO_EMAIL_FOR_USER, "username")
                return

            if not user.email_verified:
                c.errors.add(errors.NO_VERIFIED_EMAIL, field="username")
                form.set_error(errors.NO_VERIFIED_EMAIL, "username")
                return
        else:
            user = c.user

        # check for shame banned domains
        if form.has_errors("url", errors.DOMAIN_BANNED):
            g.stats.simple_event('spam.shame.link')
            return

        # demangle URL in canonical way
        if url:
            if isinstance(url, (unicode, str)):
                form.set_inputs(url=url)
            elif isinstance(url, tuple) or isinstance(url[0], Link):
                # there's already one or more links with this URL, but
                # we're allowing mutliple submissions, so we really just
                # want the URL
                url = url[0].url

        if kind == 'link':
            if form.has_errors('url', errors.NO_URL, errors.BAD_URL):
                return

        # users can change the disable_comments on promoted links
        if ((not l or not promote.is_promoted(l))
                and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG)
                     or jquery.has_errors('ratelimit', errors.RATELIMIT))):
            return

        if kind == 'self' and form.has_errors('text', errors.TOO_LONG):
            return

        if not l:
            # creating a new promoted link
            l = promote.new_promotion(title, url if kind == 'link' else 'self',
                                      selftext if kind == 'self' else '', user,
                                      request.ip)
            l.domain_override = domain_override or None
            if c.user_is_sponsor:
                l.managed_promo = is_managed
            l._commit()
            form.redirect(promote.promo_edit_url(l))

        elif not promote.is_promo(l):
            return

        # changing link type is not allowed
        if ((l.is_self and kind == 'link')
                or (not l.is_self and kind == 'self')):
            c.errors.add(errors.NO_CHANGE_KIND, field="kind")
            form.set_error(errors.NO_CHANGE_KIND, "kind")
            return

        changed = False
        # live items can only be changed by a sponsor, and also
        # pay the cost of de-approving the link
        if not promote.is_promoted(l) or c.user_is_sponsor:
            if title and title != l.title:
                l.title = title
                changed = not c.user_is_sponsor

            if kind == 'link' and url and url != l.url:
                l.url = url
                changed = not c.user_is_sponsor

        # only trips if the title and url are changed by a non-sponsor
        if changed:
            promote.unapprove_promotion(l)

        # selftext can be changed at any time
        if kind == 'self':
            l.selftext = selftext

        # comment disabling and sendreplies is free to be changed any time.
        l.disable_comments = disable_comments
        l.sendreplies = sendreplies

        if c.user_is_sponsor:
            if (form.has_errors("media_url", errors.BAD_URL)
                    or form.has_errors("gifts_embed_url", errors.BAD_URL)):
                return

        scraper_embed = media_url_type == "scrape"
        media_url = media_url or None
        gifts_embed_url = gifts_embed_url or None

        if c.user_is_sponsor and scraper_embed and media_url != l.media_url:
            if media_url:
                media = _scrape_media(media_url,
                                      autoplay=media_autoplay,
                                      save_thumbnail=False,
                                      use_cache=True)

                if media:
                    l.set_media_object(media.media_object)
                    l.set_secure_media_object(media.secure_media_object)
                    l.media_url = media_url
                    l.gifts_embed_url = None
                    l.media_autoplay = media_autoplay
                else:
                    c.errors.add(errors.SCRAPER_ERROR, field="media_url")
                    form.set_error(errors.SCRAPER_ERROR, "media_url")
                    return
            else:
                l.set_media_object(None)
                l.set_secure_media_object(None)
                l.media_url = None
                l.gifts_embed_url = None
                l.media_autoplay = False

        if (c.user_is_sponsor and not scraper_embed
                and gifts_embed_url != l.gifts_embed_url):
            if gifts_embed_url:
                parsed = UrlParser(gifts_embed_url)
                if not is_subdomain(parsed.hostname, "redditgifts.com"):
                    c.errors.add(errors.BAD_URL, field="gifts_embed_url")
                    form.set_error(errors.BAD_URL, "gifts_embed_url")
                    return

                iframe = """
                    <iframe class="redditgifts-embed"
                            src="%(embed_url)s"
                            width="710" height="500" scrolling="no"
                            frameborder="0" allowfullscreen>
                    </iframe>
                """ % {
                    'embed_url': websafe(gifts_embed_url)
                }
                media_object = {
                    'oembed': {
                        'description': 'redditgifts embed',
                        'height': 500,
                        'html': iframe,
                        'provider_name': 'redditgifts',
                        'provider_url': 'http://www.redditgifts.com/',
                        'title': 'redditgifts secret santa 2014',
                        'type': 'rich',
                        'width': 710
                    },
                    'type': 'redditgifts'
                }
                l.set_media_object(media_object)
                l.set_secure_media_object(media_object)
                l.media_url = None
                l.gifts_embed_url = gifts_embed_url
                l.media_autoplay = False
            else:
                l.set_media_object(None)
                l.set_secure_media_object(None)
                l.media_url = None
                l.gifts_embed_url = None
                l.media_autoplay = False

        if c.user_is_sponsor:
            l.media_override = media_override
            l.domain_override = domain_override or None
            l.managed_promo = is_managed

        l._commit()
        form.redirect(promote.promo_edit_url(l))

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_add_roadblock(self, form, jquery, dates, sr):
        if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors(
                'enddate', errors.BAD_DATE, errors.BAD_DATE_RANGE)):
            return
        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.add(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_rm_roadblock(self, form, jquery, dates, sr):
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.remove(sr, sd, ed)
            jquery.refresh()

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        dates=VDateRange(['startdate', 'enddate'],
                         earliest=timedelta(days=g.min_promote_future),
                         latest=timedelta(days=g.max_promote_future),
                         reference_date=promote.promo_datetime_now,
                         business_days=True,
                         sponsor_override=True),
        link=VLink('link_id36'),
        bid=VFloat('bid', coerce=False),
        target=VPromoTarget(),
        campaign_id36=nop("campaign_id36"),
        priority=VPriority("priority"),
        location=VLocation(),
    )
    def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid,
                           target, priority, location):
        if not link:
            return

        if not target:
            # run form.has_errors to populate the errors in the response
            form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                            errors.SUBREDDIT_NOTALLOWED,
                            errors.SUBREDDIT_REQUIRED)
            form.has_errors('collection', errors.COLLECTION_NOEXIST)
            form.has_errors('targeting', errors.INVALID_TARGET)
            return

        start, end = dates or (None, None)

        if not allowed_location_and_target(location, target):
            return abort(403, 'forbidden')

        cpm = PromotionPrices.get_price(target, location)

        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE)
                or form.has_errors('enddate', errors.BAD_DATE,
                                   errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE,
                                   errors.BAD_DATE_RANGE)):
            return

        # check that start is not so late that authorization hold will expire
        if not c.user_is_sponsor:
            max_start = promote.get_max_startdate()
            if start > max_start:
                c.errors.add(
                    errors.DATE_TOO_LATE,
                    msg_params={'day': max_start.strftime("%m/%d/%Y")},
                    field='startdate')
                form.has_errors('startdate', errors.DATE_TOO_LATE)
                return

        # Limit the number of PromoCampaigns a Link can have
        # Note that the front end should prevent the user from getting
        # this far
        existing_campaigns = list(PromoCampaign._by_link(link._id))
        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:
            c.errors.add(errors.TOO_MANY_CAMPAIGNS,
                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},
                         field='title')
            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
            return

        campaign = None
        if campaign_id36:
            try:
                campaign = PromoCampaign._byID36(campaign_id36)
            except NotFound:
                pass

        if campaign and link._id != campaign.link_id:
            return abort(404, 'not found')

        if priority.cpm:
            min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
            max_bid = None if c.user_is_sponsor else g.max_promote_bid

            if bid is None or bid < min_bid or (max_bid and bid > max_bid):
                c.errors.add(errors.BAD_BID,
                             field='bid',
                             msg_params={
                                 'min': min_bid,
                                 'max': max_bid or g.max_promote_bid
                             })
                form.has_errors('bid', errors.BAD_BID)
                return

            # you cannot edit the bid of a live ad unless it's a freebie
            if (campaign and bid != campaign.bid
                    and promote.is_live_promo(link, campaign)
                    and not campaign.is_freebie()):
                c.errors.add(errors.BID_LIVE, field='bid')
                form.has_errors('bid', errors.BID_LIVE)
                return

        else:
            bid = 0.  # Set bid to 0 as dummy value

        is_frontpage = (not target.is_collection
                        and target.subreddit_name == Frontpage.name)

        if not target.is_collection and not is_frontpage:
            # targeted to a single subreddit, check roadblock
            sr = target.subreddits_slow[0]
            roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end)
            if roadblock and not c.user_is_sponsor:
                msg_params = {
                    "start": roadblock[0].strftime('%m/%d/%Y'),
                    "end": roadblock[1].strftime('%m/%d/%Y')
                }
                c.errors.add(errors.OVERSOLD,
                             field='sr',
                             msg_params=msg_params)
                form.has_errors('sr', errors.OVERSOLD)
                return

        # Check inventory
        campaign = campaign if campaign_id36 else None
        if not priority.inventory_override:
            oversold = has_oversold_error(form, campaign, start, end, bid, cpm,
                                          target, location)
            if oversold:
                return

        if campaign:
            promote.edit_campaign(link, campaign, dates, bid, cpm, target,
                                  priority, location)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, target,
                                            priority, location)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(VSponsor('link_id36'),
                   VModhash(),
                   l=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_delete_campaign(self, form, jquery, l, campaign):
        if not campaign or not l or l._id != campaign.link_id:
            return abort(404, 'not found')

        promote.delete_campaign(l, campaign)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_terminate_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        promote.terminate_campaign(link, campaign)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(VSponsor('link'),
                   VModhash(),
                   link=VByName("link"),
                   campaign=VPromoCampaign("campaign"),
                   customer_id=VInt("customer_id", min=0),
                   pay_id=VInt("account", min=0),
                   edit=VBoolean("edit"),
                   address=ValidAddress([
                       "firstName", "lastName", "company", "address", "city",
                       "state", "zip", "country", "phoneNumber"
                   ]),
                   creditcard=ValidCard(
                       ["cardNumber", "expirationDate", "cardCode"]))
    def POST_update_pay(self, form, jquery, link, campaign, customer_id,
                        pay_id, edit, address, creditcard):
        if not g.authorizenetapi:
            return

        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        # Check inventory
        if campaign_has_oversold_error(form, campaign):
            return

        # check that start is not so late that authorization hold will expire
        max_start = promote.get_max_startdate()
        if campaign.start_date > max_start:
            msg = _("please change campaign start date to %(date)s or earlier")
            date = format_date(max_start, format="short", locale=c.locale)
            msg %= {'date': date}
            form.set_text(".status", msg)
            return

        # check the campaign start date is still valid (user may have created
        # the campaign a few days ago)
        now = promote.promo_datetime_now()
        min_start = now + timedelta(days=g.min_promote_future)
        if campaign.start_date.date() < min_start.date():
            msg = _("please change campaign start date to %(date)s or later")
            date = format_date(min_start, format="short", locale=c.locale)
            msg %= {'date': date}
            form.set_text(".status", msg)
            return

        address_modified = not pay_id or edit
        if address_modified:
            address_fields = [
                "firstName", "lastName", "company", "address", "city", "state",
                "zip", "country", "phoneNumber"
            ]
            card_fields = ["cardNumber", "expirationDate", "cardCode"]

            if (form.has_errors(address_fields, errors.BAD_ADDRESS)
                    or form.has_errors(card_fields, errors.BAD_CARD)):
                return

            pay_id = edit_profile(c.user, address, creditcard, pay_id)

        reason = None
        if pay_id:
            success, reason = promote.auth_campaign(link, campaign, c.user,
                                                    pay_id)

            if success:
                form.redirect(promote.promo_edit_url(link))
                return

        msg = reason or _("failed to authenticate card. sorry.")
        form.set_text(".status", msg)

    @validate(VSponsor("link_name"),
              VModhash(),
              link=VByName('link_name'),
              file=VUploadLength('file', 500 * 1024),
              img_type=VImageType('img_type'))
    def POST_link_thumb(self, link=None, file=None, img_type='jpg'):
        if link and (not promote.is_promoted(link) or c.user_is_sponsor):
            errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="")

            # thumnails for promoted links can change and therefore expire
            force_thumbnail(link, file, file_type=".%s" % img_type)

            if any(errors.values()):
                return UploadedImage("",
                                     "",
                                     "upload",
                                     errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'),
                                     thumbnail_url(link),
                                     "",
                                     errors=errors,
                                     form_id="image-upload").render()
Exemple #26
0
class LiveUpdateEventsController(RedditController):
    def GET_home(self):
        return pages.LiveUpdateMetaPage(
            title=_("reddit live"),
            content=pages.LiveUpdateHome(),
            page_classes=["liveupdate-home"],
        ).render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/api/live/happening_now",
    )
    def GET_happening_now(self):
        """ Get some basic information about the currently featured live thread.

            Returns an empty 204 response for api requests if no thread is currently featured.

            See also: [/api/live/*thread*/about](#GET_api_live_{thread}_about).
        """

        if not is_api():
            self.abort404()

        featured_event = get_featured_event()
        if not featured_event:
            response.status_code = 204
            return

        c.liveupdate_event = featured_event
        content = Wrapped(featured_event)
        return pages.LiveUpdateEventPage(content).render()

    @validate(
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateEvent("after"),
        before=VLiveUpdateEvent("before"),
        count=VCount("count"),
    )
    def GET_listing(self, filter, num, after, before, count):
        reverse = False
        if before:
            after = before
            reverse = True

        builder_cls = LiveUpdateEventBuilder
        wrapper = Wrapped
        listing_cls = Listing
        require_employee = True  # for grepping: this is used like VEmployee

        if filter == "open":
            title = _("live threads")
            query = queries.get_live_events("new", "all")
        elif filter == "closed":
            title = _("closed threads")
            query = queries.get_complete_events("new", "all")
        elif filter == "active":
            title = _("most active threads")
            query = queries.get_active_events()
        elif filter == "reported":
            if not c.user_is_admin:
                self.abort403()

            title = _("reported threads")
            query = queries.get_reported_events()
            builder_cls = LiveUpdateReportedEventBuilder
            wrapper = pages.LiveUpdateReportedEventRow
            listing_cls = pages.LiveUpdateReportedEventListing
        elif filter == "happening_now":
            featured_events = get_all_featured_events()

            title = _("featured threads")
            query = sorted(set(featured_events.values()))
            builder_cls = featured_event_builder_factory(featured_events)
            wrapper = pages.LiveUpdateFeaturedEvent
            require_employee = False
        elif filter == "mine":
            if not c.user_is_loggedin:
                self.abort404()

            title = _("my live threads")
            query = queries.get_contributor_events(c.user)
            require_employee = False
        else:
            self.abort404()

        if require_employee and not c.user.employee:
            self.abort403()

        builder = builder_cls(
            query,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
            wrap=wrapper,
            skip=True,
        )

        listing = listing_cls(builder)

        return pages.LiveUpdateMetaPage(
            title=title,
            content=listing.listing(),
        ).render()

    @validate(
        VUser(),
    )
    def GET_create(self):
        return pages.LiveUpdateMetaPage(
            title=_("create live thread"),
            content=pages.LiveUpdateCreate(),
        ).render()

    @require_oauth2_scope("submit")
    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="liveupdate_create_"),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
        uri="/api/live/create",
    )
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        amqp.add_item("new_liveupdate_event", json.dumps({
            "event_fullname": event._fullname,
            "creator_fullname": c.user._fullname,
        }))

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
Exemple #27
0
class PromoteController(ListingController):
    where = 'promoted'
    render_cls = PromotePage

    @property
    def title_text(self):
        return _('promoted by you')

    @classmethod
    @memoize('live_by_subreddit', time=300)
    def live_by_subreddit(cls, sr):
        if sr == Frontpage:
            sr_id = ''
        else:
            sr_id = sr._id
        r = LiveAdWeights.get([sr_id])
        return [i.link for i in r[sr_id]]

    @classmethod
    @memoize('subreddits_with_promos', time=3600)
    def subreddits_with_promos(cls):
        sr_ids = LiveAdWeights.get_live_subreddits()
        srs = Subreddit._byID(sr_ids, return_dict=False)
        sr_names = sorted([sr.name for sr in srs], key=lambda s: s.lower())
        return sr_names

    @property
    def menus(self):
        filters = [
            NamedButton('all_promos', dest=''),
            NamedButton('future_promos'),
            NamedButton('unpaid_promos'),
            NamedButton('rejected_promos'),
            NamedButton('pending_promos'),
            NamedButton('live_promos'),
        ]
        menus = [
            NavMenu(filters,
                    base_path='/promoted',
                    title='show',
                    type='lightdrop')
        ]

        if self.sort == 'live_promos' and c.user_is_sponsor:
            sr_names = self.subreddits_with_promos()
            buttons = [NavButton(name, name) for name in sr_names]
            frontbutton = NavButton('FRONTPAGE',
                                    Frontpage.name,
                                    aliases=[
                                        '/promoted/live_promos/%s' %
                                        urllib.quote(Frontpage.name)
                                    ])
            buttons.insert(0, frontbutton)
            buttons.insert(0, NavButton('all', ''))
            menus.append(
                NavMenu(buttons,
                        base_path='/promoted/live_promos',
                        title='subreddit',
                        type='lightdrop'))

        return menus

    def keep_fn(self):
        def keep(item):
            if item.promoted and not item._deleted:
                return True
            else:
                return False

        return keep

    def query(self):
        if c.user_is_sponsor:
            if self.sort == "future_promos":
                return queries.get_all_unapproved_links()
            elif self.sort == "pending_promos":
                return queries.get_all_accepted_links()
            elif self.sort == "unpaid_promos":
                return queries.get_all_unpaid_links()
            elif self.sort == "rejected_promos":
                return queries.get_all_rejected_links()
            elif self.sort == "live_promos" and self.sr:
                return self.live_by_subreddit(self.sr)
            elif self.sort == 'live_promos':
                return queries.get_all_live_links()
            elif self.sort == 'underdelivered':
                q = queries.get_underdelivered_campaigns()
                campaigns = PromoCampaign._by_fullname(list(q),
                                                       data=True,
                                                       return_dict=False)
                link_ids = [camp.link_id for camp in campaigns]
                return [Link._fullname_from_id36(to36(id)) for id in link_ids]
            return queries.get_all_promoted_links()
        else:
            if self.sort == "future_promos":
                return queries.get_unapproved_links(c.user._id)
            elif self.sort == "pending_promos":
                return queries.get_accepted_links(c.user._id)
            elif self.sort == "unpaid_promos":
                return queries.get_unpaid_links(c.user._id)
            elif self.sort == "rejected_promos":
                return queries.get_rejected_links(c.user._id)
            elif self.sort == "live_promos":
                return queries.get_live_links(c.user._id)
            return queries.get_promoted_links(c.user._id)

    @validate(VSponsor(), sr=nop('sr'))
    def GET_listing(self, sr=None, sort="", **env):
        if not c.user_is_loggedin or not c.user.email_verified:
            return self.redirect("/ad_inq")
        self.sort = sort
        self.sr = None
        if sr and sr == Frontpage.name:
            self.sr = Frontpage
        elif sr:
            try:
                self.sr = Subreddit._by_name(sr)
            except NotFound:
                pass
        return ListingController.GET_listing(self, **env)

    GET_index = GET_listing

    @validate(VSponsor())
    def GET_new_promo(self):
        return PromotePage('content', content=PromoteLinkNew()).render()

    @validate(VSponsor('link'), link=VLink('link'))
    def GET_edit_promo(self, link):
        if not link or link.promoted is None:
            return self.abort404()
        rendered = wrap_links(link,
                              wrapper=promote.sponsor_wrapper,
                              skip=False)
        form = PromoteLinkForm(link, rendered)
        page = PromotePage('new_promo', content=form)
        return page.render()

    # admin only because the route might change
    @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign'))
    def GET_edit_promo_campaign(self, campaign):
        if not campaign:
            return self.abort404()
        link = Link._byID(campaign.link_id)
        return self.redirect(promote.promo_edit_url(link))

    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, start, end):
        sr = sr or Frontpage
        available_by_datestr = inventory.get_available_pageviews(sr,
                                                                 start,
                                                                 end,
                                                                 datestr=True)
        return {'inventory': available_by_datestr}

    @validate(VSponsor(),
              dates=VDateRange(["startdate", "enddate"],
                               max_range=timedelta(days=28),
                               required=False))
    def GET_graph(self, dates):
        start, end, bad_dates = _check_dates(dates)
        return PromotePage("graph",
                           content=Promote_Graph(
                               start, end, bad_dates=bad_dates)).render()

    @validate(VSponsorAdmin(),
              dates=VDateRange(["startdate", "enddate"],
                               max_range=timedelta(days=28),
                               required=False))
    def GET_admingraph(self, dates):
        start, end, bad_dates = _check_dates(dates)
        content = Promote_Graph(start,
                                end,
                                bad_dates=bad_dates,
                                admin_view=True)
        if c.render_style == 'csv':
            return content.as_csv()
        return PromotePage("admingraph", content=content).render()

    # ## POST controllers below
    @validatedForm(VSponsorAdmin(),
                   link=VLink("link_id"),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_freebie(self, form, jquery, link, campaign):
        if campaign_has_oversold_error(form, campaign):
            form.set_html(".freebie", "target oversold, can't freebie")
            return

        if promote.is_promo(link) and campaign:
            promote.free_campaign(link, campaign, c.user)
            form.redirect(promote.promo_edit_url(link))

    @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note"))
    def POST_promote_note(self, form, jquery, link, note):
        if promote.is_promo(link):
            text = PromotionLog.add(link, note)
            form.find(".notes").children(":last").after("<p>" + text + "</p>")

    @noresponse(VSponsorAdmin(), thing=VByName('id'))
    def POST_promote(self, thing):
        if promote.is_promo(thing):
            promote.accept_promotion(thing)

    @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason"))
    def POST_unpromote(self, thing, reason):
        if promote.is_promo(thing):
            promote.reject_promotion(thing, reason=reason)

    @validate(VSponsorAdmin(),
              link=VLink("link"),
              campaign=VPromoCampaign("campaign"))
    def GET_refund(self, link, campaign):
        if campaign.link_id != link._id:
            return self.abort404()

        content = RefundPage(link, campaign)
        return Reddit("refund", content=content, show_sidebar=False).render()

    @validatedForm(VSponsorAdmin(),
                   link=VLink('link'),
                   campaign=VPromoCampaign('campaign'))
    def POST_refund_campaign(self, form, jquery, link, campaign):
        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign,
                                                      billable_impressions)
        refund_amount = campaign.bid - billable_amount
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount)
            form.set_html('.status', _('refund succeeded'))
        else:
            form.set_html('.status', _('refund not needed'))

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   VRatelimit(rate_user=True,
                              rate_ip=True,
                              prefix='create_promo_'),
                   VShamedDomain('url'),
                   l=VLink('link_id'),
                   title=VTitle('title'),
                   url=VUrl('url', allow_self=False, lookup=False),
                   ip=ValidIP(),
                   disable_comments=VBoolean("disable_comments"),
                   media_width=VInt("media-width", min=0),
                   media_height=VInt("media-height", min=0),
                   media_embed=VLength("media-embed", 1000),
                   media_override=VBoolean("media-override"),
                   domain_override=VLength("domain", 100))
    def POST_edit_promo(self, form, jquery, ip, l, title, url,
                        disable_comments, media_height, media_width,
                        media_embed, media_override, domain_override):

        should_ratelimit = False
        if not c.user_is_sponsor:
            should_ratelimit = True

        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        # check for shame banned domains
        if form.has_errors("url", errors.DOMAIN_BANNED):
            g.stats.simple_event('spam.shame.link')
            return

        # demangle URL in canonical way
        if url:
            if isinstance(url, (unicode, str)):
                form.set_inputs(url=url)
            elif isinstance(url, tuple) or isinstance(url[0], Link):
                # there's already one or more links with this URL, but
                # we're allowing mutliple submissions, so we really just
                # want the URL
                url = url[0].url

        # users can change the disable_comments on promoted links
        if ((not l or not promote.is_promoted(l))
                and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG)
                     or form.has_errors('url', errors.NO_URL, errors.BAD_URL)
                     or jquery.has_errors('ratelimit', errors.RATELIMIT))):
            return

        if not l:
            l = promote.new_promotion(title, url, c.user, ip)
        elif promote.is_promo(l):
            changed = False
            # live items can only be changed by a sponsor, and also
            # pay the cost of de-approving the link
            trusted = c.user_is_sponsor or c.user.trusted_sponsor
            if not promote.is_promoted(l) or trusted:
                if title and title != l.title:
                    l.title = title
                    changed = not trusted
                if url and url != l.url:
                    l.url = url
                    changed = not trusted

            # only trips if the title and url are changed by a non-sponsor
            if changed and not promote.is_unpaid(l):
                promote.unapprove_promotion(l)
            if trusted and promote.is_unapproved(l):
                promote.accept_promotion(l)

            # comment disabling is free to be changed any time.
            l.disable_comments = disable_comments
            if c.user_is_sponsor or c.user.trusted_sponsor:
                if media_embed and media_width and media_height:
                    l.media_object = dict(height=media_height,
                                          width=media_width,
                                          content=media_embed,
                                          type='custom')
                else:
                    l.media_object = None

                l.media_override = media_override
                if getattr(l, "domain_override", False) or domain_override:
                    l.domain_override = domain_override
            l._commit()

        form.redirect(promote.promo_edit_url(l))

    @validate(VSponsorAdmin())
    def GET_roadblock(self):
        return PromotePage('content', content=Roadblocks()).render()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    future=1,
                                    reference_date=promote.promo_datetime_now,
                                    business_days=False,
                                    sponsor_override=True),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_add_roadblock(self, form, jquery, dates, sr):
        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE)
                or form.has_errors('enddate', errors.BAD_DATE,
                                   errors.BAD_FUTURE_DATE,
                                   errors.BAD_DATE_RANGE)):
            return
        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.add(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    future=1,
                                    reference_date=promote.promo_datetime_now,
                                    business_days=False,
                                    sponsor_override=True),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_rm_roadblock(self, form, jquery, dates, sr):
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.remove(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    future=1,
                                    reference_date=promote.promo_datetime_now,
                                    business_days=False,
                                    sponsor_override=True),
                   link=VLink('link_id'),
                   bid=VBid('bid',
                            min=0,
                            max=g.max_promote_bid,
                            coerce=False,
                            error=errors.BAD_BID),
                   sr=VSubmitSR('sr', promotion=True),
                   campaign_id36=nop("campaign_id36"),
                   targeting=VLength("targeting", 10))
    def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid,
                           sr, targeting):
        if not link:
            return

        start, end = dates or (None, None)

        author = Account._byID(link.author_id, data=True)
        cpm = author.cpm_selfserve_pennies

        if (start and end and not promote.is_accepted(link)
                and not c.user_is_sponsor):
            # if the ad is not approved already, ensure the start date
            # is at least 2 days in the future
            start = start.date()
            end = end.date()
            now = promote.promo_datetime_now()
            future = make_offset_date(now,
                                      g.min_promote_future,
                                      business_days=True)
            if start < future.date():
                c.errors.add(errors.BAD_FUTURE_DATE,
                             msg_params=dict(day=g.min_promote_future),
                             field="startdate")

        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE)
                or form.has_errors('enddate', errors.BAD_DATE,
                                   errors.BAD_FUTURE_DATE,
                                   errors.BAD_DATE_RANGE)):
            return

        # Limit the number of PromoCampaigns a Link can have
        # Note that the front end should prevent the user from getting
        # this far
        existing_campaigns = list(PromoCampaign._by_link(link._id))
        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:
            c.errors.add(errors.TOO_MANY_CAMPAIGNS,
                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},
                         field='title')
            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
            return

        if form.has_errors('bid', errors.BAD_BID):
            return

        if campaign_id36:
            # you cannot edit the bid of a live ad unless it's a freebie
            try:
                campaign = PromoCampaign._byID36(campaign_id36)
                if (bid != campaign.bid
                        and campaign.start_date < datetime.now(g.tz)
                        and not campaign.is_freebie()):
                    c.errors.add(errors.BID_LIVE, field='bid')
                    form.has_errors('bid', errors.BID_LIVE)
                    return
            except NotFound:
                pass

        min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
        if bid is None or bid < min_bid:
            c.errors.add(errors.BAD_BID,
                         field='bid',
                         msg_params={
                             'min': min_bid,
                             'max': g.max_promote_bid
                         })
            form.has_errors('bid', errors.BAD_BID)
            return

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

            roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end)
            if roadblock and not c.user_is_sponsor:
                msg_params = {
                    "start": roadblock[0].strftime('%m/%d/%Y'),
                    "end": roadblock[1].strftime('%m/%d/%Y')
                }
                c.errors.add(errors.OVERSOLD,
                             field='sr',
                             msg_params=msg_params)
                form.has_errors('sr', errors.OVERSOLD)
                return

        elif targeting == 'none':
            sr = None

        # Check inventory
        campaign_id = campaign._id if campaign_id36 else None
        if has_oversold_error(form, campaign_id, start, end, bid, cpm, sr):
            return

        if campaign_id36 is not None:
            campaign = PromoCampaign._byID36(campaign_id36)
            promote.edit_campaign(link, campaign, dates, bid, cpm, sr)
            r = promote.get_renderable_campaigns(link, campaign)
            jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date,
                                   r.duration, r.bid, r.spent, r.cpm, r.sr,
                                   r.status)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, sr)
            r = promote.get_renderable_campaigns(link, campaign)
            jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date,
                                r.duration, r.bid, r.spent, r.cpm, r.sr,
                                r.status)

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   l=VLink('link_id'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_delete_campaign(self, form, jquery, l, campaign):
        if l and campaign:
            promote.delete_campaign(l, campaign)

    @validatedForm(VSponsor('container'),
                   VModhash(),
                   user=VExistingUname('name'),
                   thing=VByName('container'))
    def POST_traffic_viewer(self, form, jquery, user, thing):
        """
        Adds a user to the list of users allowed to view a promoted
        link's traffic page.
        """
        if not form.has_errors("name", errors.USER_DOESNT_EXIST,
                               errors.NO_USER):
            form.set_inputs(name="")
            form.set_html(".status:first", _("added"))
            if promote.add_traffic_viewer(thing, user):
                user_row = TrafficViewerList(thing).user_row(
                    'traffic_viewer', user)
                jquery(".traffic_viewer-table").show().find(
                    "table").insert_table_rows(user_row)

                # send the user a message
                msg = user_added_messages['traffic']['pm']['msg']
                subj = user_added_messages['traffic']['pm']['subject']
                if msg and subj:
                    d = dict(url=thing.make_permalink_slow(),
                             traffic_url=promote.promo_traffic_url(thing),
                             title=thing.title)
                    msg = msg % d
                    item, inbox_rel = Message._new(c.user, user, subj, msg,
                                                   request.ip)
                    queries.new_message(item, inbox_rel)

    @validatedForm(VSponsor('container'),
                   VModhash(),
                   iuser=VByName('id'),
                   thing=VByName('container'))
    def POST_rm_traffic_viewer(self, form, jquery, iuser, thing):
        if thing and iuser:
            promote.rm_traffic_viewer(thing, iuser)

    @validatedForm(
        VSponsor('link'),
        link=VByName("link"),
        campaign=VPromoCampaign("campaign"),
        customer_id=VInt("customer_id", min=0),
        pay_id=VInt("account", min=0),
        edit=VBoolean("edit"),
        address=ValidAddress([
            "firstName", "lastName", "company", "address", "city", "state",
            "zip", "country", "phoneNumber"
        ],
                             allowed_countries=g.allowed_pay_countries),
        creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"]))
    def POST_update_pay(self, form, jquery, link, campaign, customer_id,
                        pay_id, edit, address, creditcard):
        # Check inventory
        if campaign_has_oversold_error(form, campaign):
            return

        address_modified = not pay_id or edit
        form_has_errors = False
        if address_modified:
            if (form.has_errors([
                    "firstName", "lastName", "company", "address", "city",
                    "state", "zip", "country", "phoneNumber"
            ], errors.BAD_ADDRESS) or form.has_errors(
                ["cardNumber", "expirationDate", "cardCode"],
                    errors.BAD_CARD)):
                form_has_errors = True
            elif g.authorizenetapi:
                pay_id = edit_profile(c.user, address, creditcard, pay_id)
            else:
                pay_id = 1
        # if link is in use or finished, don't make a change
        if pay_id and not form_has_errors:
            # valid bid and created or existing bid id.
            # check if already a transaction
            if g.authorizenetapi:
                success, reason = promote.auth_campaign(
                    link, campaign, c.user, pay_id)
            else:
                success = True
            if success:
                form.redirect(promote.promo_edit_url(link))
            else:
                form.set_html(
                    ".status", reason
                    or _("failed to authenticate card.  sorry."))

    @validate(VSponsor("link"),
              link=VLink("link"),
              campaign=VPromoCampaign("campaign"))
    def GET_pay(self, link, campaign):
        # no need for admins to play in the credit card area
        if c.user_is_loggedin and c.user._id != link.author_id:
            return self.abort404()

        if not campaign.link_id == link._id:
            return self.abort404()
        if g.authorizenetapi:
            data = get_account_info(c.user)
            content = PaymentForm(link,
                                  campaign,
                                  customer_id=data.customerProfileId,
                                  profiles=data.paymentProfiles,
                                  max_profiles=PROFILE_LIMIT)
        else:
            content = None
        res = LinkInfoPage(link=link, content=content, show_sidebar=False)
        return res.render()

    def GET_link_thumb(self, *a, **kw):
        """
        See GET_upload_sr_image for rationale
        """
        return "nothing to see here."

    @validate(VSponsor("link_id"),
              link=VByName('link_id'),
              file=VLength('file', 500 * 1024),
              img_type=VImageType('img_type'))
    def POST_link_thumb(self, link=None, file=None, img_type='jpg'):
        if link and (not promote.is_promoted(link) or c.user_is_sponsor
                     or c.user.trusted_sponsor):
            errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="")
            try:
                # thumnails for promoted links can change and therefore expire
                force_thumbnail(link, file, file_type=".%s" % img_type)
            except cssfilter.BadImage:
                # if the image doesn't clean up nicely, abort
                errors["IMAGE_ERROR"] = _("bad image")
            if any(errors.values()):
                return UploadedImage("",
                                     "",
                                     "upload",
                                     errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'),
                                     thumbnail_url(link),
                                     "",
                                     errors=errors,
                                     form_id="image-upload").render()

    @validate(VSponsorAdmin(),
              launchdate=VDate('ondate'),
              dates=VDateRange(['startdate', 'enddate']),
              query_type=VOneOf('q', ('started_on', 'between'), default=None))
    def GET_admin(self, launchdate=None, dates=None, query_type=None):
        return PromoAdminTool(query_type=query_type,
                              launchdate=launchdate,
                              start=dates[0],
                              end=dates[1]).render()

    @validate(VSponsorAdminOrAdminSecret('secret'),
              start=VDate('startdate'),
              end=VDate('enddate'),
              link_text=nop('link_text'),
              owner=VAccountByName('owner'))
    def GET_report(self, start, end, link_text=None, owner=None):
        now = datetime.now(g.tz).replace(hour=0,
                                         minute=0,
                                         second=0,
                                         microsecond=0)
        end = end or now - timedelta(days=1)
        start = start or end - timedelta(days=7)

        links = []
        bad_links = []
        owner_name = owner.name if owner else ''

        if owner:
            promo_weights = PromotionWeights.get_campaigns(start,
                                                           end,
                                                           author_id=owner._id)
            campaign_ids = [pw.promo_idx for pw in promo_weights]
            campaigns = PromoCampaign._byID(campaign_ids, data=True)
            link_ids = {camp.link_id for camp in campaigns.itervalues()}
            links.extend(Link._byID(link_ids, data=True, return_dict=False))

        if link_text is not None:
            id36s = link_text.replace(',', ' ').split()
            try:
                links_from_text = Link._byID36(id36s, data=True)
            except NotFound:
                links_from_text = {}

            bad_links = [id36 for id36 in id36s if id36 not in links_from_text]
            links.extend(links_from_text.values())

        content = PromoteReport(links, link_text, owner_name, bad_links, start,
                                end)
        if c.render_style == 'csv':
            return content.as_csv()
        else:
            return PromotePage('report', content=content).render()
Exemple #28
0
class OAuth2AccessController(MinimalController):
    handles_csrf = True

    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        if request.method != "OPTIONS":
            c.oauth2_client = self._get_client_auth()

    def _get_client_auth(self):
        auth = request.headers.get("Authorization")
        try:
            client_id, client_secret = parse_http_basic(auth)
            require(client_id)
            client = OAuth2Client.get_token(client_id)
            require(client)
            if client.is_confidential():
                require(client_secret)
                require(constant_time_compare(client.secret, client_secret))
            return client
        except RequirementException:
            abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])

    @validate(
        grant_type=VOneOf("grant_type", (
            "authorization_code",
            "refresh_token",
            "password",
            "client_credentials",
            "https://oauth.reddit.com/grants/installed_client",
        )), )
    def POST_access_token(self, grant_type):
        """
        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code
        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for
        an access token.

        On success, returns a URL-encoded dictionary containing
        **access_token**, **token_type**, **expires_in**, and **scope**.
        If an authorization code for a permanent grant was given, a
        **refresh_token** will be included. If there is a problem, an **error**
        parameter will be returned instead.

        Must be called using SSL, and must contain a HTTP `Authorization:`
        header which contains the application's client identifier as the
        username and client secret as the password.  (The client id and secret
        are visible on the [app preferences page](/prefs/apps).)

        Per the OAuth specification, **grant_type** must be one of:

        * ``authorization_code`` for the initial access token ("standard" OAuth2 flow)
        * ``refresh_token`` for renewing the access token.
        * ``password`` for script-type apps using password auth
        * ``client_credentials`` for application-only (signed out) access - confidential clients
        * ``https://oauth.reddit.com/grants/installed_client`` extension grant for application-only (signed out)
                access - non-confidential (installed) clients

        **redirect_uri** must exactly match the value that was used in the call
        to [/api/v1/authorize](#api_method_authorize) that created this grant.

        See reddit's [OAuth2 wiki](https://github.com/reddit/reddit/wiki/OAuth2) for
        more information.

        """
        if grant_type == "authorization_code":
            return self._access_token_code()
        elif grant_type == "refresh_token":
            return self._access_token_refresh()
        elif grant_type == "password":
            return self._access_token_password()
        elif grant_type == "client_credentials":
            return self._access_token_client_credentials()
        elif grant_type == "https://oauth.reddit.com/grants/installed_client":
            return self._access_token_extension_client_credentials()
        else:
            resp = {"error": "unsupported_grant_type"}
            return self.api_wrapper(resp)

    def _check_for_errors(self):
        resp = {}
        if (errors.INVALID_OPTION, "scope") in c.errors:
            resp["error"] = "invalid_scope"
        else:
            resp["error"] = "invalid_request"
        return resp

    @classmethod
    def _make_token_dict(cls, access_token, refresh_token=None):
        if not access_token:
            return {"error": "invalid_grant"}
        expires_in = int(access_token._ttl) if access_token._ttl else None
        resp = {
            "access_token": access_token._id,
            "token_type": access_token.token_type,
            "expires_in": expires_in,
            "scope": access_token.scope,
        }
        if refresh_token:
            resp["refresh_token"] = refresh_token._id
        return resp

    @validate(code=nop("code"),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI))
    def _access_token_code(self, code, redirect_uri):
        if not code:
            c.errors.add("NO_TEXT", field="code")
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        access_token = None
        refresh_token = None

        auth_token = OAuth2AuthorizationCode.use_token(code,
                                                       c.oauth2_client._id,
                                                       redirect_uri)
        if auth_token:
            if auth_token.refreshable:
                refresh_token = OAuth2RefreshToken._new(
                    auth_token.client_id, auth_token.user_id, auth_token.scope)
            access_token = OAuth2AccessToken._new(
                auth_token.client_id, auth_token.user_id, auth_token.scope,
                refresh_token._id if refresh_token else "")

        resp = self._make_token_dict(access_token, refresh_token)

        return self.api_wrapper(resp)

    @validate(refresh_token=VOAuth2RefreshToken("refresh_token"))
    def _access_token_refresh(self, refresh_token):
        access_token = None
        if refresh_token:
            if refresh_token.client_id == c.oauth2_client._id:
                access_token = OAuth2AccessToken._new(
                    refresh_token.client_id,
                    refresh_token.user_id,
                    refresh_token.scope,
                    refresh_token=refresh_token._id)
            else:
                c.errors.add(errors.OAUTH2_INVALID_REFRESH_TOKEN)
        else:
            c.errors.add("NO_TEXT", field="refresh_token")

        if c.errors:
            resp = self._check_for_errors()
            response.status = 400
        else:
            resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(user=VThrottledLogin(["username", "password"]),
              scope=nop("scope"))
    def _access_token_password(self, user, scope):
        # username:password auth via OAuth is only allowed for
        # private use scripts
        client = c.oauth2_client
        if client.app_type != "script":
            return self.api_wrapper({
                "error":
                "unauthorized_client",
                "error_description":
                "Only script apps may use password auth"
            })
        dev_ids = client._developer_ids
        if not user or user._id not in dev_ids:
            return self.api_wrapper({"error": "invalid_grant"})
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(client._id, user._id36, scope)
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        scope=nop("scope"), )
    def _access_token_client_credentials(self, scope):
        client = c.oauth2_client
        if not client.is_confidential():
            return self.api_wrapper({
                "error":
                "unauthorized_client",
                "error_description":
                "Only confidential clients may use client_credentials auth"
            })
        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(
            client._id,
            "",
            scope,
        )
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        scope=nop("scope"),
        device_id=VLength("device_id", 50, min_length=20),
    )
    def _access_token_extension_client_credentials(self, scope, device_id):
        if ((errors.NO_TEXT, "device_id") in c.errors
                or (errors.TOO_SHORT, "device_id") in c.errors
                or (errors.TOO_LONG, "device_id") in c.errors):
            return self.api_wrapper({
                "error": "invalid_request",
                "error_description": "bad device_id",
            })

        client = c.oauth2_client
        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(
            client._id,
            "",
            scope,
            device_id=device_id,
        )
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix="rate_revoke_token_"),
        token_id=nop("token"),
        token_hint=VOneOf("token_type_hint",
                          ("access_token", "refresh_token")),
    )
    def POST_revoke_token(self, token_id, token_hint):
        '''Revoke an OAuth2 access or refresh token.

        token_type_hint is optional, and hints to the server
        whether the passed token is a refresh or access token.

        A call to this endpoint is considered a success if
        the passed `token_id` is no longer valid. Thus, if an invalid
        `token_id` was passed in, a successful 204 response will be returned.

        See [RFC7009](http://tools.ietf.org/html/rfc7009)

        '''
        # In success cases, this endpoint returns no data.
        response.status = 204

        if not token_id:
            return

        types = (OAuth2AccessToken, OAuth2RefreshToken)
        if token_hint == "refresh_token":
            types = reversed(types)

        for token_type in types:
            try:
                token = token_type._byID(token_id)
            except tdb_cassandra.NotFound:
                continue
            else:
                break
        else:
            # No Token found. The given token ID is already gone
            # or never existed. Either way, from the client's perspective,
            # the passed in token is no longer valid.
            return

        if constant_time_compare(token.client_id, c.oauth2_client._id):
            token.revoke()
        else:
            # RFC 7009 is not clear on how to handle this case.
            # Given that a malicious client could do much worse things
            # with a valid token then revoke it, returning an error
            # here is best as it may help certain clients debug issues
            response.status = 400
            return self.api_wrapper({"error": "unauthorized_client"})
Exemple #29
0
class PromoteController(ListingController):
    where = 'promoted'
    render_cls = PromotePage

    @property
    def title_text(self):
        return _('promoted by you')

    def keep_fn(self):
        def keep(item):
            if item.promoted and not item._deleted:
                return True
            else:
                return False
        return keep

    def query(self):
        if c.user_is_sponsor:
            if self.sort == "future_promos":
                return queries.get_all_unapproved_links()
            elif self.sort == "pending_promos":
                return queries.get_all_accepted_links()
            elif self.sort == "unpaid_promos":
                return queries.get_all_unpaid_links()
            elif self.sort == "rejected_promos":
                return queries.get_all_rejected_links()
            elif self.sort == "live_promos":
                return queries.get_all_live_links()
            return queries.get_all_promoted_links()
        else:
            if self.sort == "future_promos":
                return queries.get_unapproved_links(c.user._id)
            elif self.sort == "pending_promos":
                return queries.get_accepted_links(c.user._id)
            elif self.sort == "unpaid_promos":
                return queries.get_unpaid_links(c.user._id)
            elif self.sort == "rejected_promos":
                return queries.get_rejected_links(c.user._id)
            elif self.sort == "live_promos":
                return queries.get_live_links(c.user._id)
            return queries.get_promoted_links(c.user._id)

    @validate(VSponsor())
    def GET_listing(self, sort="", **env):
        if not c.user_is_loggedin or not c.user.email_verified:
            return self.redirect("/ad_inq")
        self.sort = sort
        return ListingController.GET_listing(self, **env)

    GET_index = GET_listing

    @validate(VSponsor())
    def GET_new_promo(self):
        return PromotePage('content', content=PromoteLinkForm()).render()

    @validate(VSponsor('link'),
              link=VLink('link'))
    def GET_edit_promo(self, link):
        if not link or link.promoted is None:
            return self.abort404()
        rendered = wrap_links(link, wrapper=promote.sponsor_wrapper,
                              skip=False)

        form = PromoteLinkForm(link=link,
                               listing=rendered,
                               timedeltatext="")

        page = PromotePage('new_promo', content=form)

        return page.render()


    # For development. Should eventually replace GET_edit_promo
    @validate(VSponsor('link'),
              link=VLink('link'))
    def GET_edit_promo_cpm(self, link):
        if not link or link.promoted is None:
            return self.abort404()
        rendered = wrap_links(link, wrapper=promote.sponsor_wrapper,
                              skip=False)

        form = PromoteLinkFormCpm(link=link,
                                  listing=rendered,
                                  timedeltatext="")

        page = PromotePage('new_promo', content=form)

        return page.render()


    # admin only because the route might change
    @validate(VSponsorAdmin('campaign'),
              campaign=VPromoCampaign('campaign'))
    def GET_edit_promo_campaign(self, campaign):
        if not campaign:
            return self.abort404()
        link = Link._byID(campaign.link_id)
        return self.redirect(promote.promo_edit_url(link))

    @validate(VSponsor(),
              dates=VDateRange(["startdate", "enddate"],
                               max_range=timedelta(days=28),
                               required=False))
    def GET_graph(self, dates):
        start, end, bad_dates = _check_dates(dates)
        return PromotePage("graph",
                           content=Promote_Graph(
                                start, end, bad_dates=bad_dates)
                           ).render()

    @validate(VSponsorAdmin(),
              dates=VDateRange(["startdate", "enddate"],
                               max_range=timedelta(days=28),
                               required=False))
    def GET_admingraph(self, dates):
        start, end, bad_dates = _check_dates(dates)
        content = Promote_Graph(start, end, bad_dates=bad_dates,
                                admin_view=True)
        if c.render_style == 'csv':
            return content.as_csv()
        return PromotePage("admingraph", content=content).render()

    def GET_inventory(self, sr_name):
        '''
        Return available inventory data as json for use in ajax calls
        '''
        inv_start_date = promote.promo_datetime_now()
        inv_end_date = inv_start_date + timedelta(60)
        inventory = promote.get_available_impressions(
            sr_name,
            inv_start_date,
            inv_end_date,
            fuzzed=(not c.user_is_admin)
        )
        dates = []
        impressions = []
        max_imps = 0
        for date, imps in inventory.iteritems():
            dates.append(date.strftime("%m/%d/%Y"))
            impressions.append(imps)
            max_imps = max(max_imps, imps)
        return json.dumps({'sr':sr_name,
                           'dates': dates,
                           'imps':impressions,
                           'max_imps':max_imps})

    # ## POST controllers below
    @validatedForm(VSponsorAdmin(),
                   link=VLink("link_id"),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_freebie(self, form, jquery, link, campaign):
        if promote.is_promo(link) and campaign:
            promote.free_campaign(link, campaign, c.user)
            form.redirect(promote.promo_edit_url(link))

    @validatedForm(VSponsorAdmin(),
                   link=VByName("link"),
                   note=nop("note"))
    def POST_promote_note(self, form, jquery, link, note):
        if promote.is_promo(link):
            text = PromotionLog.add(link, note)
            form.find(".notes").children(":last").after(
                "<p>" + text + "</p>")


    @noresponse(VSponsorAdmin(),
                thing=VByName('id'))
    def POST_promote(self, thing):
        if promote.is_promo(thing):
            promote.accept_promotion(thing)

    @noresponse(VSponsorAdmin(),
                thing=VByName('id'),
                reason=nop("reason"))
    def POST_unpromote(self, thing, reason):
        if promote.is_promo(thing):
            promote.reject_promotion(thing, reason=reason)

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   VRatelimit(rate_user=True,
                              rate_ip=True,
                              prefix='create_promo_'),
                   l=VLink('link_id'),
                   title=VTitle('title'),
                   url=VUrl('url', allow_self=False, lookup=False),
                   ip=ValidIP(),
                   disable_comments=VBoolean("disable_comments"),
                   set_clicks=VBoolean("set_maximum_clicks"),
                   max_clicks=VInt("maximum_clicks", min=0),
                   set_views=VBoolean("set_maximum_views"),
                   max_views=VInt("maximum_views", min=0),
                   media_width=VInt("media-width", min=0),
                   media_height=VInt("media-height", min=0),
                   media_embed=VLength("media-embed", 1000),
                   media_override=VBoolean("media-override"),
                   domain_override=VLength("domain", 100)
                   )
    def POST_edit_promo(self, form, jquery, ip, l, title, url,
                        disable_comments,
                        set_clicks, max_clicks,
                        set_views, max_views,
                        media_height, media_width, media_embed,
                        media_override, domain_override):

        should_ratelimit = False
        if not c.user_is_sponsor:
            set_clicks = False
            set_views = False
            should_ratelimit = True
        if not set_clicks:
            max_clicks = None
        if not set_views:
            max_views = None

        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        # demangle URL in canonical way
        if url:
            if isinstance(url, (unicode, str)):
                form.set_inputs(url=url)
            elif isinstance(url, tuple) or isinstance(url[0], Link):
                # there's already one or more links with this URL, but
                # we're allowing mutliple submissions, so we really just
                # want the URL
                url = url[0].url

        # users can change the disable_comments on promoted links
        if ((not l or not promote.is_promoted(l)) and
            (form.has_errors('title', errors.NO_TEXT,
                            errors.TOO_LONG) or
            form.has_errors('url', errors.NO_URL, errors.BAD_URL) or
            jquery.has_errors('ratelimit', errors.RATELIMIT))):
            return

        if not l:
            l = promote.new_promotion(title, url, c.user, ip)
        elif promote.is_promo(l):
            changed = False
            # live items can only be changed by a sponsor, and also
            # pay the cost of de-approving the link
            trusted = c.user_is_sponsor or c.user.trusted_sponsor
            if not promote.is_promoted(l) or trusted:
                if title and title != l.title:
                    l.title = title
                    changed = not trusted
                if url and url != l.url:
                    l.url = url
                    changed = not trusted

            # only trips if the title and url are changed by a non-sponsor
            if changed and not promote.is_unpaid(l):
                promote.unapprove_promotion(l)
            if trusted and promote.is_unapproved(l):
                promote.accept_promotion(l)

            if c.user_is_sponsor:
                l.maximum_clicks = max_clicks
                l.maximum_views = max_views

            # comment disabling is free to be changed any time.
            l.disable_comments = disable_comments
            if c.user_is_sponsor or c.user.trusted_sponsor:
                if media_embed and media_width and media_height:
                    l.media_object = dict(height=media_height,
                                          width=media_width,
                                          content=media_embed,
                                          type='custom')
                else:
                    l.media_object = None

                l.media_override = media_override
                if getattr(l, "domain_override", False) or domain_override:
                    l.domain_override = domain_override
            l._commit()

        form.redirect(promote.promo_edit_url(l))

    @validate(VSponsorAdmin())
    def GET_roadblock(self):
        return PromotePage('content', content=Roadblocks()).render()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                      future=1,
                                      reference_date=promote.promo_datetime_now,
                                      business_days=False,
                                      sponsor_override=True),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_add_roadblock(self, form, jquery, dates, sr):
        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE) or
            form.has_errors('enddate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)):
            return
        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return
        if dates and sr:
            sd, ed = dates
            promote.roadblock_reddit(sr.name, sd.date(), ed.date())
            jquery.refresh()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                      future=1,
                                      reference_date=promote.promo_datetime_now,
                                      business_days=False,
                                      sponsor_override=True),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_rm_roadblock(self, form, jquery, dates, sr):
        if dates and sr:
            sd, ed = dates
            promote.unroadblock_reddit(sr.name, sd.date(), ed.date())
            jquery.refresh()

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                  future=1,
                                  reference_date=promote.promo_datetime_now,
                                  business_days=False,
                                  sponsor_override=True),
                   l=VLink('link_id'),
                   bid=VFloat('bid', min=0, max=g.max_promote_bid,
                                  coerce=False, error=errors.BAD_BID),
                   sr=VSubmitSR('sr', promotion=True),
                   campaign_id36=nop("campaign_id36"),
                   targeting=VLength("targeting", 10))
    def POST_edit_campaign(self, form, jquery, l, campaign_id36,
                          dates, bid, sr, targeting):
        if not l:
            return

        start, end = dates or (None, None)

        if (start and end and not promote.is_accepted(l) and
            not c.user_is_sponsor):
            # if the ad is not approved already, ensure the start date
            # is at least 2 days in the future
            start = start.date()
            end = end.date()
            now = promote.promo_datetime_now()
            future = make_offset_date(now, g.min_promote_future,
                                      business_days=True)
            if start < future.date():
                c.errors.add(errors.BAD_FUTURE_DATE,
                             msg_params=dict(day=g.min_promote_future),
                             field="startdate")


        if (form.has_errors('startdate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE) or
            form.has_errors('enddate', errors.BAD_DATE,
                            errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)):
            return

        # Limit the number of PromoCampaigns a Link can have
        # Note that the front end should prevent the user from getting
        # this far
        existing_campaigns = list(PromoCampaign._by_link(l._id))
        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:
            c.errors.add(errors.TOO_MANY_CAMPAIGNS,
                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},
                         field='title')
            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
            return

        duration = max((end - start).days, 1)

        if form.has_errors('bid', errors.BAD_BID):
            return

        # minimum bid depends on user privilege and targeting, checked here
        # instead of in the validator b/c current duration is needed
        if c.user_is_sponsor:
            min_daily_bid = 0
        elif targeting == 'one':
            min_daily_bid = g.min_promote_bid * 1.5
        else:
            min_daily_bid = g.min_promote_bid

        if campaign_id36:
            # you cannot edit the bid of a live ad unless it's a freebie
            try:
                campaign = PromoCampaign._byID36(campaign_id36)
                if (bid != campaign.bid and
                    campaign.start_date < datetime.now(g.tz)
                    and not campaign.is_freebie()):
                    c.errors.add(errors.BID_LIVE, field='bid')
                    form.has_errors('bid', errors.BID_LIVE)
                    return
            except NotFound:
                pass

        if bid is None or bid / duration < min_daily_bid:
            c.errors.add(errors.BAD_BID, field='bid',
                         msg_params={'min': min_daily_bid,
                                       'max': g.max_promote_bid})
            form.has_errors('bid', errors.BAD_BID)
            return

        if targeting == 'one':
            if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                               errors.SUBREDDIT_NOTALLOWED,
                               errors.SUBREDDIT_REQUIRED):
                # checking to get the error set in the form, but we can't
                # check for rate-limiting if there's no subreddit
                return
            oversold = promote.is_roadblocked(sr.name, start, end)
            if oversold and not c.user_is_sponsor:
                msg_params = {"start": oversold[0].strftime('%m/%d/%Y'),
                              "end": oversold[1].strftime('%m/%d/%Y')}
                c.errors.add(errors.OVERSOLD, field='sr',
                             msg_params=msg_params)
                form.has_errors('sr', errors.OVERSOLD)
                return
        if targeting == 'none':
            sr = None

        if campaign_id36 is not None:
            campaign = PromoCampaign._byID36(campaign_id36)
            promote.edit_campaign(l, campaign, dates, bid, sr)
            r = promote.get_renderable_campaigns(l, campaign)
            jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date,
                                   r.duration, r.bid, r.sr, r.status)
        else:
            campaign = promote.new_campaign(l, dates, bid, sr)
            r = promote.get_renderable_campaigns(l, campaign)
            jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date,
                                r.duration, r.bid, r.sr, r.status)

    @validatedForm(VSponsor('link_id'),
                   VModhash(),
                   l=VLink('link_id'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_delete_campaign(self, form, jquery, l, campaign):
        if l and campaign:
            promote.delete_campaign(l, campaign)


    @validatedForm(VSponsor('container'),
                   VModhash(),
                   user=VExistingUname('name'),
                   thing=VByName('container'))
    def POST_traffic_viewer(self, form, jquery, user, thing):
        """
        Adds a user to the list of users allowed to view a promoted
        link's traffic page.
        """
        if not form.has_errors("name",
                               errors.USER_DOESNT_EXIST, errors.NO_USER):
            form.set_inputs(name="")
            form.set_html(".status:first", _("added"))
            if promote.add_traffic_viewer(thing, user):
                user_row = TrafficViewerList(thing).user_row('traffic', user)
                jquery("#traffic-table").show(
                    ).find("table").insert_table_rows(user_row)

                # send the user a message
                msg = user_added_messages['traffic']['pm']['msg']
                subj = user_added_messages['traffic']['pm']['subject']
                if msg and subj:
                    d = dict(url=thing.make_permalink_slow(),
                             traffic_url=promote.promo_traffic_url(thing),
                             title=thing.title)
                    msg = msg % d
                    item, inbox_rel = Message._new(c.user, user,
                                                   subj, msg, request.ip)
                    queries.new_message(item, inbox_rel)


    @validatedForm(VSponsor('container'),
                   VModhash(),
                   iuser=VByName('id'),
                   thing=VByName('container'))
    def POST_rm_traffic_viewer(self, form, jquery, iuser, thing):
        if thing and iuser:
            promote.rm_traffic_viewer(thing, iuser)


    @validatedForm(VSponsor('link'),
                   link=VByName("link"),
                   campaign=VPromoCampaign("campaign"),
                   customer_id=VInt("customer_id", min=0),
                   pay_id=VInt("account", min=0),
                   edit=VBoolean("edit"),
                   address=ValidAddress(
                    ["firstName", "lastName", "company", "address",
                     "city", "state", "zip", "country", "phoneNumber"],
                    allowed_countries=g.allowed_pay_countries),
                   creditcard=ValidCard(["cardNumber", "expirationDate",
                                           "cardCode"]))
    def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id,
                        edit, address, creditcard):
        address_modified = not pay_id or edit
        form_has_errors = False
        if address_modified:
            if (form.has_errors(["firstName", "lastName", "company", "address",
                                 "city", "state", "zip",
                                 "country", "phoneNumber"],
                                errors.BAD_ADDRESS) or
                form.has_errors(["cardNumber", "expirationDate", "cardCode"],
                                errors.BAD_CARD)):
                form_has_errors = True
            elif g.authorizenetapi:
                pay_id = edit_profile(c.user, address, creditcard, pay_id)
            else:
                pay_id = 1
        # if link is in use or finished, don't make a change
        if pay_id and not form_has_errors:
            # valid bid and created or existing bid id.
            # check if already a transaction
            if g.authorizenetapi:
                success, reason = promote.auth_campaign(link, campaign, c.user,
                                                        pay_id)
            else:
                success = True
            if success:
                form.redirect(promote.promo_edit_url(link))
            else:
                form.set_html(".status",
                              reason or
                              _("failed to authenticate card.  sorry."))

    @validate(VSponsor("link"),
              link=VLink("link"),
              campaign=VPromoCampaign("campaign"))
    def GET_pay(self, link, campaign):
        # no need for admins to play in the credit card area
        if c.user_is_loggedin and c.user._id != link.author_id:
            return self.abort404()

        if not campaign.link_id == link._id:
            return self.abort404()
        if g.authorizenetapi:
            data = get_account_info(c.user)
            content = PaymentForm(link, campaign,
                                  customer_id=data.customerProfileId,
                                  profiles=data.paymentProfiles,
                                  max_profiles=PROFILE_LIMIT)
        else:
            content = None
        res = LinkInfoPage(link=link,
                            content=content,
                            show_sidebar=False)
        return res.render()

    def GET_link_thumb(self, *a, **kw):
        """
        See GET_upload_sr_image for rationale
        """
        return "nothing to see here."

    @validate(VSponsor("link_id"),
              link=VByName('link_id'),
              file=VLength('file', 500 * 1024))
    def POST_link_thumb(self, link=None, file=None):
        if link and (not promote.is_promoted(link) or
                     c.user_is_sponsor or c.user.trusted_sponsor):
            errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="")
            try:
                # thumnails for promoted links can change and therefore expire
                force_thumbnail(link, file, file_type=".jpg")
            except cssfilter.BadImage:
                # if the image doesn't clean up nicely, abort
                errors["IMAGE_ERROR"] = _("bad image")
            if any(errors.values()):
                return UploadedImage("", "", "upload", errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'), thumbnail_url(link), "",
                                     errors=errors,
                                     form_id="image-upload").render()

    @validate(VSponsorAdmin(),
              launchdate=VDate('ondate'),
              dates=VDateRange(['startdate', 'enddate']),
              query_type=VOneOf('q', ('started_on', 'between'), default=None))
    def GET_admin(self, launchdate=None, dates=None, query_type=None):
        return PromoAdminTool(query_type=query_type,
                              launchdate=launchdate,
                              start=dates[0],
                              end=dates[1]).render()