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)
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_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)
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)
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)
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)
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, )
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({})
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({})
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)
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
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)
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)
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)
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()
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)
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()
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"})
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()