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)
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)
class AdzerkApiController(api.ApiController): @csrf_exempt @validate( srnames=VPrintable("srnames", max_length=2100), is_mobile_web=VBoolean('is_mobile_web'), ) def POST_request_promo(self, srnames, is_mobile_web): self.OPTIONS_request_promo() if not srnames: return srnames = srnames.split('+') # request multiple ads in case some are hidden by the builder due # to the user's hides/preferences response = adzerk_request(srnames, mobile_web=is_mobile_web) if not response: g.stats.simple_event('adzerk.request.no_promo') return res_by_campaign = {r.campaign: r for r in response} tuples = [promote.PromoTuple(r.link, 1., r.campaign) for r in response] builder = CampaignBuilder(tuples, wrap=default_thing_wrapper(), keep_fn=promote.promo_keep_fn, num=1, skip=True) listing = LinkListing(builder, nextprev=False).listing() promote.add_trackers(listing.things, c.site) if listing.things: g.stats.simple_event('adzerk.request.valid_promo') w = listing.things[0] r = res_by_campaign[w.campaign] up = UrlParser(r.imp_pixel) up.hostname = "pixel.redditmedia.com" w.adserver_imp_pixel = up.unparse() w.adserver_click_url = r.click_url w.num = "" return responsive(w.render(), space_compress=True) else: g.stats.simple_event('adzerk.request.skip_promo')
class BetaModeController(RedditController): @validate( VUser(), name=VPrintable('name', 15), ) def GET_beta(self, name): user_exempt = beta_user_exempt(c.user) if name != g.beta_name or (g.beta_require_admin and not user_exempt): abort(404) content = BetaSettings( beta_name=g.beta_name, beta_title=g.beta_title, description_md=g.beta_description_md[0], feedback_sr=g.beta_feedback_sr, enabled=c.beta, require_gold=g.beta_require_gold and not user_exempt, has_gold=c.user_is_loggedin and c.user.gold, ) return BoringPage( pagename=g.beta_title, content_id='beta-settings', content=content, show_sidebar=False, ).render() def GET_disable(self, **kwargs): # **kwargs included above to swallow pylons env arguments passed in # due to argspec inspection of decorator **kwargs. return BoringPage( pagename=_('disabling beta'), content_id='beta-disable', content=BetaDisable(), show_sidebar=False, ).render()
class WikiApiController(WikiController): @require_oauth2_scope("wikiedit") @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=nop(('content')), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256, empty_error=None)) @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True) def POST_wiki_edit(self, pageandprevious, content, page_name, reason): """Edit a wiki `page`""" page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') if renderer in ('wiki', 'reddit'): content = VMarkdown(('content'), renderer=renderer).run(content) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.POST.get('previous') try: # special validation methods if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content, verify=False) if g.css_killswitch: self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if css_errors: error_items = [CssError(x).message for x in css_errors] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) elif page.name == "config/automoderator": try: rules = Ruleset(content) except ValueError as e: error_items = [e.message] self.handle_error(415, "SPECIAL_ERRORS", special_errors=error_items) # special saving methods if page.name == "config/stylesheet": c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special and page.name in ATTRIBUTE_BY_PAGE: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, "wikirevise", details=description, description=reason) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/{act}', uses_site=True, uri_variants=[ '/api/wiki/alloweditor/%s' % act for act in ('del', 'add') ]) def POST_wiki_allow_editor(self, act, page, user): """Allow/deny `username` to edit this wiki `page`""" if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate( VModhash(), VAdmin(), pv=VWikiPageAndVersion(('page', 'revision')), deleted=VBoolean('deleted'), ) def POST_wiki_revision_delete(self, pv, deleted): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') if deleted and page.revision == str(revision._id): self.handle_error(400, 'REVISION_IS_CURRENT') revision.admin_deleted = deleted revision._commit() return json.dumps({'status': revision.admin_deleted}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True) def POST_wiki_revision_hide(self, pv): """Toggle the public visibility of a wiki page revision""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True) def POST_wiki_revision_revert(self, pv): """Revert a wiki `page` to `revision`""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content) if css_errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')
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({})
class StripeController(GoldPaymentController): name = 'stripe' webhook_secret = g.STRIPE_WEBHOOK_SECRET event_type_mappings = { 'charge.succeeded': 'succeeded', 'charge.failed': 'failed', 'charge.refunded': 'refunded', 'customer.created': 'noop', 'transfer.created': 'noop', 'transfer.paid': 'noop', } @classmethod def process_response(cls): event_dict = json.loads(request.body) event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY) status = event.type if cls.event_type_mappings.get(status) == 'noop': return status, None, None, None, None charge = event.data.object description = charge.description try: passthrough, buyer_name = description.split('-', 1) except ValueError: g.log.error('stripe_error on charge: %s', charge) raise transaction_id = 'S%s' % charge.id pennies = charge.amount months, days = months_and_days_from_pennies(pennies) return status, passthrough, transaction_id, pennies, months @validatedForm(VUser(), token=nop('stripeToken'), passthrough=VPrintable("passthrough", max_length=50), pennies=VInt('pennies'), months=VInt("months")) def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months): """ Submit charge to stripe. Called by GoldPayment form. This submits the charge to stripe, and gold will be applied once we receive a webhook from stripe. """ try: payment_blob = validate_blob(passthrough) except GoldException as e: # This should never happen. All fields in the payment_blob # are validated on creation form.set_html('.status', _('something bad happened, try again later')) g.log.debug('POST_goldcharge: %s' % e.message) return penny_months, days = months_and_days_from_pennies(pennies) if not months or months != penny_months: form.set_html('.status', _('stop trying to trick the form')) return stripe.api_key = g.STRIPE_SECRET_KEY try: customer = stripe.Customer.create(card=token) if (customer['active_card']['address_line1_check'] == 'fail' or customer['active_card']['address_zip_check'] == 'fail'): form.set_html('.status', _('error: address verification failed')) form.find('.stripe-submit').removeAttr('disabled').end() return if customer['active_card']['cvc_check'] == 'fail': form.set_html('.status', _('error: cvc check failed')) form.find('.stripe-submit').removeAttr('disabled').end() return charge = stripe.Charge.create( amount=pennies, currency="usd", customer=customer['id'], description='%s-%s' % (passthrough, c.user.name) ) except stripe.CardError as e: form.set_html('.status', 'error: %s' % e.message) form.find('.stripe-submit').removeAttr('disabled').end() except stripe.InvalidRequestError as e: form.set_html('.status', _('invalid request')) except stripe.APIConnectionError as e: form.set_html('.status', _('api error')) except stripe.AuthenticationError as e: form.set_html('.status', _('connection error')) except stripe.StripeError as e: form.set_html('.status', _('error')) g.log.error('stripe error: %s' % e) else: form.set_html('.status', _('payment submitted')) # webhook usually sends near instantly, send a message in case subject = _('gold payment') msg = _('your payment is being processed and gold will be' ' delivered shortly') send_system_message(c.user, subject, msg)
class GoldPaymentController(RedditController): name = '' webhook_secret = '' event_type_mappings = {} @textresponse(secret=VPrintable('secret', 50)) def POST_goldwebhook(self, secret): self.validate_secret(secret) res = self.process_response() status, passthrough, transaction_id, pennies, months = res try: event_type = self.event_type_mappings[status] except KeyError: g.log.error('%s %s: unknown status %s' % (self.name, transaction_id, status)) self.abort403() self.process_webhook(event_type, passthrough, transaction_id, pennies, months) def validate_secret(self, secret): if secret != self.webhook_secret: g.log.error('%s: invalid webhook secret from %s' % (self.name, request.ip)) self.abort403() @classmethod def process_response(cls): """Extract status, passthrough, transaction_id, pennies.""" raise NotImplementedError def process_webhook(self, event_type, passthrough, transaction_id, pennies, months): if event_type == 'noop': return try: payment_blob = validate_blob(passthrough) except GoldError as e: g.log.error('%s %s: bad payment_blob %s' % (self.name, transaction_id, e)) self.abort403() goldtype = payment_blob['goldtype'] buyer = payment_blob['buyer'] recipient = payment_blob.get('recipient', None) signed = payment_blob.get('signed', False) giftmessage = payment_blob.get('giftmessage', None) comment = payment_blob.get('comment', None) comment = comment._fullname if comment else None existing = retrieve_gold_transaction(transaction_id) if event_type == 'cancelled': subject = 'gold payment cancelled' msg = ('your gold payment has been cancelled, contact ' '%(gold_email)s for details' % {'gold_email': g.goldthanks_email}) send_system_message(buyer, subject, msg) if existing: # note that we don't check status on existing, probably # should update gold_table when a cancellation happens reverse_gold_purchase(transaction_id) elif event_type == 'succeeded': if existing and existing.status == 'processed': g.log.info('POST_goldwebhook skipping %s' % transaction_id) return payer_email = '' payer_id = '' subscription_id = None complete_gold_purchase(passthrough, transaction_id, payer_email, payer_id, subscription_id, pennies, months, goldtype, buyer, recipient, signed, giftmessage, comment) elif event_type == 'failed': subject = 'gold payment failed' msg = ('your gold payment has failed, contact %(gold_email)s for ' 'details' % {'gold_email': g.goldthanks_email}) send_system_message(buyer, subject, msg) # probably want to update gold_table here elif event_type == 'refunded': if not (existing and existing.status == 'processed'): return subject = 'gold refund' msg = ('your gold payment has been refunded, contact ' '%(gold_email)s for details' % {'gold_email': g.goldthanks_email}) send_system_message(buyer, subject, msg) reverse_gold_purchase(transaction_id)
class IpnController(RedditController): # Used when buying gold with creddits @validatedForm(VUser(), months = VInt("months"), passthrough = VPrintable("passthrough", max_length=50)) def POST_spendcreddits(self, form, jquery, months, passthrough): if months is None or months < 1: form.set_html(".status", _("nice try.")) return days = months * 31 if not passthrough: raise ValueError("/spendcreddits got no passthrough?") blob_key, payment_blob = get_blob(passthrough) if payment_blob["goldtype"] != "gift": raise ValueError("/spendcreddits payment_blob %s has goldtype %s" % (passthrough, payment_blob["goldtype"])) signed = payment_blob["signed"] giftmessage = _force_unicode(payment_blob["giftmessage"]) recipient_name = payment_blob["recipient"] if payment_blob["account_id"] != c.user._id: fmt = ("/spendcreddits payment_blob %s has userid %d " + "but c.user._id is %d") raise ValueError(fmt % passthrough, payment_blob["account_id"], c.user._id) try: recipient = Account._by_name(recipient_name) except NotFound: raise ValueError("Invalid username %s in spendcreddits, buyer = %s" % (recipient_name, c.user.name)) if recipient._deleted: form.set_html(".status", _("that user has deleted their account")) return if not c.user_is_admin: if months > c.user.gold_creddits: raise ValueError("%s is trying to sneak around the creddit check" % c.user.name) c.user.gold_creddits -= months c.user.gold_creddit_escrow += months c.user._commit() comment_id = payment_blob.get("comment") comment = send_gift(c.user, recipient, months, days, signed, giftmessage, comment_id) if not c.user_is_admin: c.user.gold_creddit_escrow -= months c.user._commit() payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30) form.set_html(".status", _("the gold has been delivered!")) form.find("button").hide() if comment: gilding_message = make_comment_gold_message(comment, user_gilded=True) jquery.gild_comment(comment_id, gilding_message, comment.gildings) @textresponse(full_sn = VLength('serial-number', 100)) def POST_gcheckout(self, full_sn): if full_sn: short_sn = full_sn.split('-')[0] g.log.error( "GOOGLE CHECKOUT: %s" % short_sn) trans = _google_ordernum_request(short_sn) # get the financial details auth = trans.find("authorization-amount-notification") custom = None cart = trans.find("shopping-cart") if cart: private_item_data = cart.find("merchant-private-item-data") if private_item_data: custom = str(private_item_data.contents[0]) if not auth: # see if the payment was declinded status = trans.findAll('financial-order-state') if 'PAYMENT_DECLINED' in [x.contents[0] for x in status]: g.log.error("google declined transaction found: '%s'" % short_sn) elif 'REVIEWING' not in [x.contents[0] for x in status]: g.log.error(("google transaction not found: " + "'%s', status: %s") % (short_sn, [x.contents[0] for x in status])) else: g.log.error(("google transaction status: " + "'%s', status: %s") % (short_sn, [x.contents[0] for x in status])) if custom: payment_blob = validate_blob(custom) buyer = payment_blob['buyer'] subject = _('gold order') msg = _('your order has been received and gold will' ' be delivered shortly. please bear with us' ' as google wallet payments can take up to an' ' hour to complete') try: send_system_message(buyer, subject, msg) except MessageError: g.log.error('gcheckout send_system_message failed') elif auth.find("financial-order-state" ).contents[0] == "CHARGEABLE": email = str(auth.find("email").contents[0]) payer_id = str(auth.find('buyer-id').contents[0]) if custom: days = None try: pennies = int(float(trans.find("order-total" ).contents[0])*100) months, days = months_and_days_from_pennies(pennies) if not months: raise ValueError("Bad pennies for %s" % short_sn) charged = trans.find("charge-amount-notification") if not charged: _google_charge_and_ship(short_sn) parameters = request.POST.copy() self.finish(parameters, "g%s" % short_sn, email, payer_id, None, custom, pennies, months, days) except ValueError, e: g.log.error(e) else: raise ValueError("Got no custom blob for %s" % short_sn) return (('<notification-acknowledgment ' + 'xmlns="http://checkout.google.com/schema/2" ' + 'serial-number="%s" />') % full_sn) else:
self.finish(parameters, "g%s" % short_sn, email, payer_id, None, custom, pennies, months, days) except ValueError, e: g.log.error(e) else: raise ValueError("Got no custom blob for %s" % short_sn) return (('<notification-acknowledgment ' + 'xmlns="http://checkout.google.com/schema/2" ' + 'serial-number="%s" />') % full_sn) else: g.log.error("GOOGLE CHCEKOUT: didn't work") g.log.error(repr(list(request.POST.iteritems()))) @textresponse(paypal_secret = VPrintable('secret', 50), payment_status = VPrintable('payment_status', 20), txn_id = VPrintable('txn_id', 20), paying_id = VPrintable('payer_id', 50), payer_email = VPrintable('payer_email', 250), mc_currency = VPrintable('mc_currency', 20), mc_gross = VFloat('mc_gross'), custom = VPrintable('custom', 50)) def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id, payer_email, mc_currency, mc_gross, custom): parameters = request.POST.copy() # Make sure it's really PayPal if paypal_secret != g.PAYPAL_SECRET: log_text("invalid IPN secret",
class StripeController(GoldPaymentController): name = 'stripe' webhook_secret = g.STRIPE_WEBHOOK_SECRET event_type_mappings = { 'charge.succeeded': 'succeeded', 'charge.failed': 'failed', 'charge.refunded': 'refunded', 'charge.dispute.created': 'noop', 'charge.dispute.updated': 'noop', 'charge.dispute.closed': 'noop', 'customer.created': 'noop', 'customer.card.created': 'noop', 'customer.card.deleted': 'noop', 'transfer.created': 'noop', 'transfer.paid': 'noop', 'balance.available': 'noop', 'invoice.created': 'noop', 'invoice.updated': 'noop', 'invoice.payment_succeeded': 'noop', 'invoice.payment_failed': 'failed_subscription', 'invoiceitem.deleted': 'noop', 'customer.subscription.created': 'noop', 'customer.deleted': 'noop', 'customer.updated': 'noop', 'customer.subscription.deleted': 'noop', 'customer.subscription.trial_will_end': 'noop', 'customer.subscription.updated': 'noop', 'dummy': 'noop', } @classmethod def process_response(cls): event_dict = json.loads(request.body) event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY) status = event.type if status == 'invoice.created': # sent 1 hr before a subscription is charged or immediately for # a new subscription invoice = event.data.object customer_id = invoice.customer account = account_from_stripe_customer_id(customer_id) # if the charge hasn't been attempted (meaning this is 1 hr before # the charge) check that the account can receive the gold if (not invoice.attempted and (not account or (account and account._banned))): # there's no associated account - delete the subscription # to cancel the charge g.log.error('no account for stripe invoice: %s', invoice) try: customer = stripe.Customer.retrieve(customer_id) customer.delete() except stripe.InvalidRequestError: pass elif status == 'invoice.payment_failed': invoice = event.data.object customer_id = invoice.customer buyer = account_from_stripe_customer_id(customer_id) webhook = Webhook(subscr_id=customer_id, buyer=buyer) return status, webhook event_type = cls.event_type_mappings.get(status) if not event_type: raise ValueError('Stripe: unrecognized status %s' % status) elif event_type == 'noop': return status, None charge = event.data.object description = charge.description invoice_id = charge.invoice transaction_id = 'S%s' % charge.id pennies = charge.amount months, days = months_and_days_from_pennies(pennies) if status == 'charge.failed' and invoice_id: # we'll get an additional failure notification event of # "invoice.payment_failed", don't double notify return 'dummy', None elif status == 'charge.failed' and not description: # create_customer can POST successfully but fail to create a # customer because the card is declined. This will trigger a # 'charge.failed' notification but without description so we can't # do anything with it return 'dummy', None elif invoice_id: # subscription charge - special handling customer_id = charge.customer buyer = account_from_stripe_customer_id(customer_id) if not buyer: charge_date = datetime.fromtimestamp(charge.created, tz=g.tz) # don't raise exception if charge date is within the past hour # db replication lag may cause the account lookup to fail if charge_date < timeago('1 hour'): raise ValueError('no buyer for charge: %s' % charge.id) else: abort(404, "not found") webhook = Webhook(transaction_id=transaction_id, subscr_id=customer_id, pennies=pennies, months=months, goldtype='autorenew', buyer=buyer) return status, webhook else: try: passthrough, buyer_name = description.split('-', 1) except (AttributeError, ValueError): g.log.error('stripe_error on charge: %s', charge) raise webhook = Webhook(passthrough=passthrough, transaction_id=transaction_id, pennies=pennies, months=months) return status, webhook @classmethod @handle_stripe_error def create_customer(cls, form, token): description = c.user.name customer = stripe.Customer.create(card=token, description=description) if (customer['active_card']['address_line1_check'] == 'fail' or customer['active_card']['address_zip_check'] == 'fail'): form.set_html('.status', _('error: address verification failed')) form.find('.stripe-submit').removeAttr('disabled').end() return None elif customer['active_card']['cvc_check'] == 'fail': form.set_html('.status', _('error: cvc check failed')) form.find('.stripe-submit').removeAttr('disabled').end() return None else: return customer @classmethod @handle_stripe_error def charge_customer(cls, form, customer, pennies, passthrough): charge = stripe.Charge.create(amount=pennies, currency="usd", customer=customer['id'], description='%s-%s' % (passthrough, c.user.name)) return charge @classmethod @handle_stripe_error def set_creditcard(cls, form, user, token): if not user.has_stripe_subscription: return customer = stripe.Customer.retrieve(user.gold_subscr_id) customer.card = token customer.save() return customer @classmethod @handle_stripe_error def set_subscription(cls, form, customer, plan_id): subscription = customer.update_subscription(plan=plan_id) return subscription @classmethod @handle_stripe_error def cancel_subscription(cls, form, user): if not user.has_stripe_subscription: return customer = stripe.Customer.retrieve(user.gold_subscr_id) customer.delete() user.gold_subscr_id = None user._commit() subject = _('your gold subscription has been cancelled') message = _('if you have any questions please email %(email)s') message %= {'email': g.goldthanks_email} send_system_message(user, subject, message) return customer @validatedForm(VUser(), token=nop('stripeToken'), passthrough=VPrintable("passthrough", max_length=50), pennies=VInt('pennies'), months=VInt("months"), period=VOneOf("period", ("monthly", "yearly"))) def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months, period): """ Submit charge to stripe. Called by GoldPayment form. This submits the charge to stripe, and gold will be applied once we receive a webhook from stripe. """ try: payment_blob = validate_blob(passthrough) except GoldException as e: # This should never happen. All fields in the payment_blob # are validated on creation form.set_html('.status', _('something bad happened, try again later')) g.log.debug('POST_goldcharge: %s' % e.message) return if period: plan_id = (g.STRIPE_MONTHLY_GOLD_PLAN if period == 'monthly' else g.STRIPE_YEARLY_GOLD_PLAN) if c.user.has_gold_subscription: form.set_html( '.status', _('your account already has a gold subscription')) return else: plan_id = None penny_months, days = months_and_days_from_pennies(pennies) if not months or months != penny_months: form.set_html('.status', _('stop trying to trick the form')) return customer = self.create_customer(form, token) if not customer: return if period: subscription = self.set_subscription(form, customer, plan_id) if not subscription: return c.user.gold_subscr_id = customer.id c.user._commit() status = _('subscription created') subject = _('reddit gold subscription') body = _('Your subscription is being processed and reddit gold ' 'will be delivered shortly.') else: charge = self.charge_customer(form, customer, pennies, passthrough) if not charge: return status = _('payment submitted') subject = _('reddit gold payment') body = _('Your payment is being processed and reddit gold ' 'will be delivered shortly.') form.set_html('.status', status) body = append_random_bottlecap_phrase(body) send_system_message(c.user, subject, body, distinguished='gold-auto') @validatedForm(VUser(), VModhash(), token=nop('stripeToken')) def POST_modify_subscription(self, form, jquery, token): customer = self.set_creditcard(form, c.user, token) if not customer: return form.set_html('.status', _('your payment details have been updated')) @validatedForm(VUser(), VModhash(), user=VByName('user')) def POST_cancel_subscription(self, form, jquery, user): if user != c.user and not c.user_is_admin: self.abort403() customer = self.cancel_subscription(form, user) if not customer: return form.set_html(".status", _("your subscription has been cancelled"))
class GoldPaymentController(RedditController): name = '' webhook_secret = '' event_type_mappings = {} @textresponse(secret=VPrintable('secret', 50)) def POST_goldwebhook(self, secret): self.validate_secret(secret) status, webhook = self.process_response() try: event_type = self.event_type_mappings[status] except KeyError: g.log.error('%s %s: unknown status %s' % (self.name, webhook, status)) self.abort403() self.process_webhook(event_type, webhook) def validate_secret(self, secret): if secret != self.webhook_secret: g.log.error('%s: invalid webhook secret from %s' % (self.name, request.ip)) self.abort403() @classmethod def process_response(cls): """Extract status and webhook.""" raise NotImplementedError def process_webhook(self, event_type, webhook): if event_type == 'noop': return existing = retrieve_gold_transaction(webhook.transaction_id) if not existing and webhook.passthrough: try: webhook.load_blob() except GoldException as e: g.log.error('%s: payment_blob %s', webhook.transaction_id, e) self.abort403() msg = None if event_type == 'cancelled': subject = _('reddit gold payment cancelled') msg = _('Your reddit gold payment has been cancelled, contact ' '%(gold_email)s for details') % { 'gold_email': g.goldthanks_email } if existing: # note that we don't check status on existing, probably # should update gold_table when a cancellation happens reverse_gold_purchase(webhook.transaction_id) elif event_type == 'succeeded': if existing and existing.status == 'processed': g.log.info('POST_goldwebhook skipping %s' % webhook.transaction_id) return self.complete_gold_purchase(webhook) elif event_type == 'failed': subject = _('reddit gold payment failed') msg = _('Your reddit gold payment has failed, contact ' '%(gold_email)s for details') % { 'gold_email': g.goldthanks_email } elif event_type == 'failed_subscription': subject = _('reddit gold subscription payment failed') msg = _('Your reddit gold subscription payment has failed. ' 'Please go to http://www.reddit.com/subscription to ' 'make sure your information is correct, or contact ' '%(gold_email)s for details') % { 'gold_email': g.goldthanks_email } elif event_type == 'refunded': if not (existing and existing.status == 'processed'): return subject = _('reddit gold refund') msg = _('Your reddit gold payment has been refunded, contact ' '%(gold_email)s for details') % { 'gold_email': g.goldthanks_email } reverse_gold_purchase(webhook.transaction_id) if msg: if existing: buyer = Account._byID(int(existing.account_id), data=True) elif webhook.buyer: buyer = webhook.buyer else: return send_system_message(buyer, subject, msg) @classmethod def complete_gold_purchase(cls, webhook): """After receiving a message from a payment processor, apply gold. Shared endpoint for all payment processing systems. Validation of gold purchase (sender, recipient, etc.) should happen before hitting this. """ secret = webhook.passthrough transaction_id = webhook.transaction_id payer_email = webhook.payer_email payer_id = webhook.payer_id subscr_id = webhook.subscr_id pennies = webhook.pennies months = webhook.months goldtype = webhook.goldtype buyer = webhook.buyer recipient = webhook.recipient signed = webhook.signed giftmessage = webhook.giftmessage comment = webhook.comment gold_recipient = recipient or buyer with gold_lock(gold_recipient): gold_recipient._sync_latest() days = days_from_months(months) if goldtype in ('onetime', 'autorenew'): admintools.engolden(buyer, days) if goldtype == 'onetime': subject = "thanks for buying reddit gold!" if g.lounge_reddit: message = strings.lounge_msg else: message = ":)" else: subject = "your reddit gold has been renewed!" message = ("see the details of your subscription on " "[your userpage](/u/%s)" % buyer.name) elif goldtype == 'creddits': buyer._incr('gold_creddits', months) subject = "thanks for buying creddits!" message = ("To spend them, visit http://%s/gold or your " "favorite person's userpage." % (g.domain)) elif goldtype == 'gift': send_gift(buyer, recipient, months, days, signed, giftmessage, comment) subject = "thanks for giving reddit gold!" message = "Your gift to %s has been delivered." % recipient.name status = 'processed' secret_pieces = [goldtype] if goldtype == 'gift': secret_pieces.append(recipient.name) secret_pieces.append(secret or transaction_id) secret = '-'.join(secret_pieces) try: create_claimed_gold(transaction_id, payer_email, payer_id, pennies, days, secret, buyer._id, c.start_time, subscr_id=subscr_id, status=status) except IntegrityError: g.log.error('gold: got duplicate gold transaction') try: message = append_random_bottlecap_phrase(message) send_system_message(buyer, subject, message, distinguished='gold-auto') except MessageError: g.log.error( 'complete_gold_purchase: send_system_message error')
class IpnController(RedditController): # Used when buying gold with creddits @validatedForm(VUser(), months=VInt("months"), passthrough=VPrintable("passthrough", max_length=50)) def POST_spendcreddits(self, form, jquery, months, passthrough): if months is None or months < 1: form.set_html(".status", _("nice try.")) return days = months * 31 if not passthrough: raise ValueError("/spendcreddits got no passthrough?") blob_key, payment_blob = get_blob(passthrough) if payment_blob["goldtype"] != "gift": raise ValueError("/spendcreddits payment_blob %s has goldtype %s" % (passthrough, payment_blob["goldtype"])) signed = payment_blob["signed"] giftmessage = _force_unicode(payment_blob["giftmessage"]) recipient_name = payment_blob["recipient"] if payment_blob["account_id"] != c.user._id: fmt = ("/spendcreddits payment_blob %s has userid %d " + "but c.user._id is %d") raise ValueError(fmt % passthrough, payment_blob["account_id"], c.user._id) try: recipient = Account._by_name(recipient_name) except NotFound: raise ValueError( "Invalid username %s in spendcreddits, buyer = %s" % (recipient_name, c.user.name)) if recipient._deleted: form.set_html(".status", _("that user has deleted their account")) return if not c.user.employee: if months > c.user.gold_creddits: raise ValueError( "%s is trying to sneak around the creddit check" % c.user.name) c.user.gold_creddits -= months c.user.gold_creddit_escrow += months c.user._commit() comment_id = payment_blob.get("comment") comment = send_gift(c.user, recipient, months, days, signed, giftmessage, comment_id) if not c.user.employee: c.user.gold_creddit_escrow -= months c.user._commit() payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30) form.set_html(".status", _("the gold has been delivered!")) form.find("button").hide() if comment: gilding_message = make_comment_gold_message(comment, user_gilded=True) jquery.gild_comment(comment_id, gilding_message, comment.gildings) @textresponse(paypal_secret=VPrintable('secret', 50), payment_status=VPrintable('payment_status', 20), txn_id=VPrintable('txn_id', 20), paying_id=VPrintable('payer_id', 50), payer_email=VPrintable('payer_email', 250), mc_currency=VPrintable('mc_currency', 20), mc_gross=VFloat('mc_gross'), custom=VPrintable('custom', 50)) def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id, payer_email, mc_currency, mc_gross, custom): parameters = request.POST.copy() # Make sure it's really PayPal if paypal_secret != g.PAYPAL_SECRET: log_text("invalid IPN secret", "%s guessed the wrong IPN secret" % request.ip, "warning") raise ValueError # Return early if it's an IPN class we don't care about response, psl = check_payment_status(payment_status) if response: return response # Return early if it's a txn_type we don't care about response, subscription = check_txn_type(parameters['txn_type'], psl) if subscription is None: subscr_id = None elif subscription == "new": subscr_id = parameters['subscr_id'] elif subscription == "cancel": cancel_subscription(parameters['subscr_id']) else: raise ValueError("Weird subscription: %r" % subscription) if response: return response # Check for the debug flag, and if so, dump the IPN dict if g.cache.get("ipn-debug"): g.cache.delete("ipn-debug") dump_parameters(parameters) if mc_currency != 'USD': raise ValueError("Somehow got non-USD IPN %r" % mc_currency) if not (txn_id and paying_id and payer_email and mc_gross): dump_parameters(parameters) raise ValueError("Got incomplete IPN") pennies = int(mc_gross * 100) months, days = months_and_days_from_pennies(pennies) # Special case: autorenewal payment existing = existing_subscription(subscr_id, paying_id, custom) if existing: if existing != "deleted account": create_claimed_gold("P" + txn_id, payer_email, paying_id, pennies, days, None, existing._id, c.start_time, subscr_id) admintools.engolden(existing, days) g.log.info("Just applied IPN renewal for %s, %d days" % (existing.name, days)) return "Ok" # More sanity checks that all non-autorenewals should pass: if not custom: dump_parameters(parameters) raise ValueError("Got IPN with txn_id=%s and no custom" % txn_id) self.finish(parameters, "P" + txn_id, payer_email, paying_id, subscr_id, custom, pennies, months, days) def finish(self, parameters, txn_id, payer_email, paying_id, subscr_id, custom, pennies, months, days): blob_key, payment_blob = get_blob(custom) buyer_id = payment_blob.get('account_id', None) if not buyer_id: dump_parameters(parameters) raise ValueError("No buyer_id in IPN with custom='%s'" % custom) try: buyer = Account._byID(buyer_id) except NotFound: dump_parameters(parameters) raise ValueError("Invalid buyer_id %d in IPN with custom='%s'" % (buyer_id, custom)) if subscr_id: buyer.gold_subscr_id = subscr_id instagift = False if payment_blob['goldtype'] in ('autorenew', 'onetime'): admintools.engolden(buyer, days) subject = _("Eureka! Thank you for investing in reddit gold!") message = _("Thank you for buying reddit gold. Your patronage " "supports the site and makes future development " "possible. For example, one month of reddit gold " "pays for 5 instance hours of reddit's servers.") message += "\n\n" + strings.gold_benefits_msg if g.lounge_reddit: message += "\n* " + strings.lounge_msg elif payment_blob['goldtype'] == 'creddits': buyer._incr("gold_creddits", months) buyer._commit() subject = _("Eureka! Thank you for investing in reddit gold " "creddits!") message = _("Thank you for buying creddits. Your patronage " "supports the site and makes future development " "possible. To spend your creddits and spread reddit " "gold, visit [/gold](/gold) or your favorite " "person's user page.") message += "\n\n" + strings.gold_benefits_msg + "\n\n" message += _("Thank you again for your support, and have fun " "spreading gold!") elif payment_blob['goldtype'] == 'gift': recipient_name = payment_blob.get('recipient', None) try: recipient = Account._by_name(recipient_name) except NotFound: dump_parameters(parameters) raise ValueError( "Invalid recipient_name %s in IPN/GC with custom='%s'" % (recipient_name, custom)) signed = payment_blob.get("signed", False) giftmessage = _force_unicode(payment_blob.get("giftmessage", "")) comment_id = payment_blob.get("comment") send_gift(buyer, recipient, months, days, signed, giftmessage, comment_id) instagift = True subject = _("Thanks for giving the gift of reddit gold!") message = _("Your classy gift to %s has been delivered.\n\n" "Thank you for gifting reddit gold. Your patronage " "supports the site and makes future development " "possible.") % recipient.name message += "\n\n" + strings.gold_benefits_msg + "\n\n" message += _("Thank you again for your support, and have fun " "spreading gold!") else: dump_parameters(parameters) raise ValueError("Got status '%s' in IPN/GC" % payment_blob['status']) # Reuse the old "secret" column as a place to record the goldtype # and "custom", just in case we need to debug it later or something secret = payment_blob['goldtype'] + "-" + custom if instagift: status = "instagift" else: status = "processed" create_claimed_gold(txn_id, payer_email, paying_id, pennies, days, secret, buyer_id, c.start_time, subscr_id, status=status) message = append_random_bottlecap_phrase(message) send_system_message(buyer, subject, message, distinguished='gold-auto') payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30)
class WikiApiController(WikiController): @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=VMarkdown(('content'), renderer='wiki'), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256)) @api_doc(api_section.wiki, uri='/api/wiki/edit') def POST_wiki_edit(self, pageandprevious, content, page_name, reason): page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.post.get('previous') try: if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content, verify=False) if report is None: # g.css_killswitch self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if report.errors: error_items = [x.message for x in sorted(report.errors)] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", str(page.revision)) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, 'wikirevise', details=description) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/:act') def POST_wiki_allow_editor(self, act, page, user): if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide') def POST_wiki_revision_hide(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert') def POST_wiki_revision_revert(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content) if report.errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", page.revision) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')
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)
class AdzerkApiController(api.ApiController): @csrf_exempt @allow_oauth2_access @validate( srnames=VPrintable("srnames", max_length=2100), is_mobile_web=VBoolean('is_mobile_web'), platform=VOneOf("platform", [ "desktop", "mobile_web", "mobile_native", ], default=None), loid=nop('loid', None), is_refresh=VBoolean("is_refresh", default=False), ) def POST_request_promo(self, srnames, is_mobile_web, platform, loid, is_refresh): self.OPTIONS_request_promo() if not srnames: return # backwards compat if platform is None: platform = "mobile_web" if is_mobile_web else "desktop" srnames = srnames.split('+') # request multiple ads in case some are hidden by the builder due # to the user's hides/preferences response = adzerk_request(srnames, self.get_uid(loid), platform=platform) if not response: g.stats.simple_event('adzerk.request.no_promo') return # for adservers, adzerk returns markup so we pass it to the client if isinstance(response, AdserverResponse): g.stats.simple_event('adzerk.request.adserver') return responsive(response.body) res_by_campaign = {r.campaign: r for r in response} adserver_click_urls = {r.campaign: r.click_url for r in response} tuples = [promote.PromoTuple(r.link, 1., r.campaign) for r in response] builder = CampaignBuilder(tuples, wrap=default_thing_wrapper(), keep_fn=promote.promo_keep_fn, num=1, skip=True) listing = LinkListing(builder, nextprev=False).listing() promote.add_trackers(listing.things, c.site, adserver_click_urls=adserver_click_urls) promote.update_served(listing.things) if listing.things: g.stats.simple_event('adzerk.request.valid_promo') if is_refresh: g.stats.simple_event('adzerk.request.auto_refresh') w = listing.things[0] r = res_by_campaign[w.campaign] up = UrlParser(r.imp_pixel) up.hostname = "pixel.redditmedia.com" w.adserver_imp_pixel = up.unparse() w.adserver_upvote_pixel = r.upvote_pixel w.adserver_downvote_pixel = r.downvote_pixel w.adserver_click_url = r.click_url w.num = "" return responsive(w.render(), space_compress=True) else: g.stats.simple_event('adzerk.request.skip_promo') def get_uid(self, loid): if c.user_is_loggedin: return c.user._id36 elif loid: return loid else: return None