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 MultiApiController(RedditController, OAuth2ResourceController): on_validation_error = staticmethod(abort_with_error) def pre(self): set_extension(request.environ, "json") self.check_for_bearer_token() RedditController.pre(self) @require_oauth2_scope("read") @validate(VUser()) @api_doc(api_section.multis, uri="/api/multi/mine") def GET_my_multis(self): """Fetch a list of multis belonging to the current user.""" multis = LabeledMulti.by_owner(c.user) wrapped = wrap_things(*multis) resp = [w.render() for w in wrapped] return self.api_wrapper(resp) def _format_multi(self, multi): resp = wrap_things(multi)[0].render() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate(multi=VMultiByPath("multipath", require_view=True)) @api_doc( api_section.multis, uri="/api/multi/{multipath}", ) def GET_multi(self, multi): """Fetch a multi's data and subreddit list by name.""" return self._format_multi(multi) def _check_new_multi_path(self, path_info): if path_info['username'].lower() != c.user.name.lower(): raise RedditError('MULTI_CANNOT_EDIT', code=403, fields='multipath') def _add_multi_srs(self, multi, sr_datas): srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas) for sr in srs.itervalues(): if isinstance(sr, FakeSubreddit): raise RedditError('MULTI_SPECIAL_SUBREDDIT', msg_params={'path': sr.path}, code=400) sr_props = {} for sr_data in sr_datas: try: sr = srs[sr_data['name']] except KeyError: raise RedditError('SUBREDDIT_NOEXIST', code=400) else: # name is passed in via the API data format, but should not be # stored on the model. del sr_data['name'] sr_props[sr] = sr_data try: multi.add_srs(sr_props) except TooManySubredditsError as e: raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409) return sr_props def _write_multi_data(self, multi, data): multi.visibility = data['visibility'] multi.clear_srs() try: self._add_multi_srs(multi, data['subreddits']) except: multi._revert() raise multi._commit() return multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def POST_multi(self, path_info, data): """Create a multi. Responds with 409 Conflict if it already exists.""" self._check_new_multi_path(path_info) try: LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], c.user) response.status = 201 else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def PUT_multi(self, path_info, data): """Create or update a multi.""" self._check_new_multi_path(path_info) try: multi = LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], c.user) response.status = 201 self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), ) @api_doc(api_section.multis, extends=GET_multi) def DELETE_multi(self, multi): """Delete a multi.""" multi.delete() def _copy_multi(self, from_multi, to_path_info): self._check_new_multi_path(to_path_info) to_owner = Account._by_name(to_path_info['username']) try: LabeledMulti._byID(to_path_info['path']) except tdb_cassandra.NotFound: to_multi = LabeledMulti.copy(to_path_info['path'], from_multi, owner=to_owner) else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') return to_multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_view=True), to_path_info=VMultiPath( "to", docs={"to": "destination multireddit url path"}, ), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/copy", ) def POST_multi_copy(self, from_multi, to_path_info): """Copy a multi. Responds with 409 Conflict if the target already exists. A "copied from ..." line will automatically be appended to the description. """ to_multi = self._copy_multi(from_multi, to_path_info) from_path = from_multi.path to_multi.copied_from = from_path if to_multi.description_md: to_multi.description_md += '\n\n' to_multi.description_md += _('copied from %(source)s') % { # force markdown linking since /user/foo is not autolinked 'source': '[%s](%s)' % (from_path, from_path) } to_multi.visibility = 'private' to_multi._commit() return self._format_multi(to_multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_edit=True), to_path_info=VMultiPath( "to", docs={"to": "destination multireddit url path"}, ), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/rename", ) def POST_multi_rename(self, from_multi, to_path_info): """Rename a multi.""" to_multi = self._copy_multi(from_multi, to_path_info) from_multi.delete() return self._format_multi(to_multi) def _get_multi_subreddit(self, multi, sr): resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True), sr=VSRByName('srname'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/r/{srname}", ) def GET_multi_subreddit(self, multi, sr): """Get data about a subreddit in a multi.""" return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr_name=VSubredditName('srname', allow_language_srs=True), data=VValidatedJSON("model", multi_sr_data_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def PUT_multi_subreddit(self, multi, sr_name, data): """Add a subreddit to a multi.""" new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs) data['name'] = sr_name sr_props = self._add_multi_srs(multi, [data]) sr = sr_props.items()[0][0] multi._commit() if new: response.status = 201 return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr=VSRByName('srname'), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def DELETE_multi_subreddit(self, multi, sr): """Remove a subreddit from a multi.""" multi.del_srs(sr) multi._commit() def _format_multi_description(self, multi): resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/description", ) def GET_multi_description(self, multi): """Get a multi's description.""" return self._format_multi_description(multi) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_edit=True), data=VValidatedJSON('model', multi_description_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_description) def PUT_multi_description(self, multi, data): """Change a multi's markdown description.""" multi.description_md = data['body_md'] multi._commit() return self._format_multi_description(multi)
class QrCodeController(RedditController): @validate( meetup=validators.VMeetup("codename"), ) def GET_portal(self, meetup): if meetup.state != "closed": if c.user_is_loggedin: content = pages.MeetupPortal(meetup=meetup) else: content = pages.LoggedOutMeetupPortal(meetup=meetup) else: content = pages.ClosedMeetupPortal(meetup=meetup) return pages.MeatspacePage( content=content, page_classes=["meatspace-portal"] ).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_configure_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.ConversationStarterSelector(meetup, c.user) return pages.MeatspacePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), topic=validators.VConversationStarter("topic"), ) def GET_badge(self, meetup, topic): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.QrCodeBadge(meetup, c.user, topic) return pages.MeatspaceBadgePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_mobile_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.MobileQrCodeBadge(meetup, c.user) return content.render() @validate( VUser(), meetup=validators.VMeetup("codename"), other=VExistingUname("user"), connected_with=VExistingUname("connected-with"), code=VInt("code"), ) def GET_connect(self, meetup, other, code, connected_with): if meetup.state not in CONNECT_STATES: self.abort404() content = pages.QrCodeForm( meetup=meetup, other=other, code=code, connected_with=connected_with, ) return pages.MeatspacePage(content=content).render() @validatedForm( VUser(), VModhash(), meetup=validators.VMeetup("codename"), other=VExistingUname("username"), code=VInt("code"), ) def POST_connect(self, form, jquery, meetup, other, code): if meetup.state not in CONNECT_STATES: self.abort403() jquery("body .connection-success").hide() if form.has_errors("username", errors.NO_USER, errors.USER_DOESNT_EXIST): return if c.user == other: c.errors.add(errors.MEETUP_NOT_WITH_SELF, field="username") form.set_error(errors.MEETUP_NOT_WITH_SELF, "username") return expected_code = utils.make_secret_code(meetup, other) if code != expected_code: g.log.warning("%r just tried an invalid code on %r", c.user.name, other.name) c.errors.add(errors.MEETUP_INVALID_CODE, field="code") form.set_error(errors.MEETUP_INVALID_CODE, "code") return models.MeetupConnections._connect(meetup, c.user, other) models.MeetupConnectionsByAccount._connect(meetup, c.user, other) g.stats.simple_event("meetup.connection") form.redirect("/meetup/%s/connect?connected-with=%s" % (meetup._id, other.name)) @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_connections(self, meetup): all_connections = models.MeetupConnectionsByAccount._connections( meetup, c.user) connections = [a for a in all_connections if not a._deleted] content = pages.QrCodeConnections( meetup=meetup, connections=connections, ) return pages.MeatspacePage(content=content).render() @validate(meetup=validators.VMeetup("codename")) def GET_connect_shortlink(self, meetup, user, code): if meetup.state not in CONNECT_STATES: self.abort404() params = urllib.urlencode({ "user": user, "code": code, }) return redirect_to("/meetup/%s/connect?%s" % (str(meetup._id), params), _code=301)
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 OAuth2FrontendController(RedditController): def check_for_bearer_token(self): pass def pre(self): RedditController.pre(self) require_https() def _check_redirect_uri(self, client, redirect_uri): if not redirect_uri or not client or redirect_uri != client.redirect_uri: abort(ForbiddenError(errors.OAUTH2_INVALID_REDIRECT_URI)) def _error_response(self, state, redirect_uri, as_fragment=False): """Return an error redirect, but only if client_id and redirect_uri are valid.""" resp = {"state": state} if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors: resp["error"] = "unauthorized_client" elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors: resp["error"] = "access_denied" elif (errors.BAD_HASH, None) in c.errors: resp["error"] = "access_denied" elif (errors.INVALID_OPTION, "response_type") in c.errors: resp["error"] = "unsupported_response_type" elif (errors.OAUTH2_INVALID_SCOPE, "scope") in c.errors: resp["error"] = "invalid_scope" else: resp["error"] = "invalid_request" final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment) return self.redirect(final_redirect, code=302) @validate(VUser(), response_type = VOneOf("response_type", ("code", "token")), client = VOAuth2ClientID(), redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope = VOAuth2Scope(), state = VRequired("state", errors.NO_TEXT), duration = VOneOf("duration", ("temporary", "permanent"), default="temporary")) def GET_authorize(self, response_type, client, redirect_uri, scope, state, duration): """ First step in [OAuth 2.0](http://oauth.net/2/) authentication. End users will be prompted for their credentials (username/password) and asked if they wish to authorize the application identified by the **client_id** parameter with the permissions specified by the **scope** parameter. They are then redirected to the endpoint on the client application's side specified by **redirect_uri**. If the user granted permission to the application, the response will contain a **code** parameter with a temporary authorization code which can be exchanged for an access token at [/api/v1/access_token](#api_method_access_token). **redirect_uri** must match the URI configured for the client in the [app preferences](/prefs/apps). If **client_id** or **redirect_uri** is not valid, or if the call does not take place over SSL, a 403 error will be returned. For all other errors, a redirect to **redirect_uri** will be returned, with a **error** parameter indicating why the request failed. """ # Check redirect URI first; it will ensure client exists self._check_redirect_uri(client, redirect_uri) if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id")) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed c.errors.add((errors.INVALID_OPTION, "duration")) if not c.errors: return OAuth2AuthorizationPage(client, redirect_uri, scope, state, duration, response_type).render() else: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) @validate(VUser(), VModhash(fatal=False), client = VOAuth2ClientID(), redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope = VOAuth2Scope(), state = VRequired("state", errors.NO_TEXT), duration = VOneOf("duration", ("temporary", "permanent"), default="temporary"), authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED), response_type = VOneOf("response_type", ("code", "token"), default="code")) def POST_authorize(self, authorize, client, redirect_uri, scope, state, duration, response_type): """Endpoint for OAuth2 authorization.""" if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id")) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed c.errors.add((errors.INVALID_OPTION, "duration")) self._check_redirect_uri(client, redirect_uri) if c.errors: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) if response_type == "code": code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id36, scope, duration == "permanent") resp = {"code": code._id, "state": state} final_redirect = _update_redirect_uri(redirect_uri, resp) elif response_type == "token": token = OAuth2AccessToken._new(client._id, c.user._id36, scope) token_data = OAuth2AccessController._make_token_dict(token) token_data["state"] = state final_redirect = _update_redirect_uri(redirect_uri, token_data, as_fragment=True) return self.redirect(final_redirect, code=302)
class LiveUpdateController(RedditController): def __before__(self, event): RedditController.__before__(self) if event: try: c.liveupdate_event = LiveUpdateEvent._byID(event) except tdb_cassandra.NotFound: pass if not c.liveupdate_event: self.abort404() if c.user_is_loggedin: c.liveupdate_permissions = \ c.liveupdate_event.get_permissions(c.user) # revoke some permissions from everyone after closing if c.liveupdate_event.state != "live": c.liveupdate_permissions = (c.liveupdate_permissions .without("update") .without("close") ) if c.user_is_admin: c.liveupdate_permissions = ContributorPermissionSet.SUPERUSER else: c.liveupdate_permissions = ContributorPermissionSet.NONE if c.liveupdate_event.banned and not c.liveupdate_permissions: error_page = RedditError( title=_("this thread has been banned"), message="", image="subreddit-banned.png", ) request.environ["usable_error_content"] = error_page.render() self.abort403() if (c.liveupdate_event.nsfw and not c.over18 and request.host != g.media_domain and # embeds are special c.render_style == "html"): return self.intermediate_redirect("/over18", sr_path=False) @require_oauth2_scope("read") @validate( num=VLimit("limit", default=25, max_limit=100), after=VLiveUpdateID("after"), before=VLiveUpdateID("before"), count=VCount("count"), is_embed=VBoolean("is_embed", docs={"is_embed": "(internal use only)"}), style_sr=VSRByName("stylesr"), ) @api_doc( section=api_section.live, uri="/live/{thread}", supports_rss=True, notes=[paginated_listing.doc_note], ) def GET_listing(self, num, after, before, count, is_embed, style_sr): """Get a list of updates posted in this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ reverse = False if before: reverse = True after = before query = LiveUpdateStream.query([c.liveupdate_event._id], count=num, reverse=reverse) if after: query.column_start = after builder = LiveUpdateBuilder(query=query, skip=True, reverse=reverse, num=num, count=count) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() if c.user_is_loggedin: report_type = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) else: report_type = None content = pages.LiveUpdateEventApp( event=c.liveupdate_event, listing=wrapped_listing, show_sidebar=not is_embed, report_type=report_type, ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + "/about.json", Wrapped(c.liveupdate_event), ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) if not is_embed: return pages.LiveUpdateEventAppPage( content=content, page_classes=['liveupdate-app'], ).render() else: # ensure we're off the cookie domain before allowing embedding if request.host != g.media_domain: abort(404) c.allow_framing = True # interstitial redirects and nsfw settings are funky on the media # domain. just disable nsfw embeds. if c.liveupdate_event.nsfw: embed_page = pages.LiveUpdateEventEmbed( content=pages.LiveUpdateNSFWEmbed(), ) request.environ["usable_error_content"] = embed_page.render() abort(403) embed_page = pages.LiveUpdateEventEmbed( content=content, page_classes=['liveupdate-app'], ) if style_sr and getattr(style_sr, "type", "private") != "private": c.can_apply_styles = True c.allow_styles = True embed_page.subreddit_stylesheet_url = \ Reddit.get_subreddit_stylesheet_url(style_sr) return embed_page.render() def GET_focus(self, target): try: target = uuid.UUID(target) except (TypeError, ValueError): self.abort404() try: query = LiveUpdateStream.query_focus(c.liveupdate_event, target) except tdb_cassandra.NotFound: self.abort404() builder = LiveUpdateBuilder( query=query, skip=True, reverse=True, num=1, count=0) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) content = pages.LiveUpdateFocusApp( event=c.liveupdate_event, listing=wrapped_listing, ) return pages.LiveUpdateEventFocusPage( content=content, page_classes=["liveupdate-focus"], ).render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/about", ) def GET_about(self): """Get some basic information about the live thread. See also: [/api/live/*thread*/edit](#POST_api_live_{thread}_edit). """ if not is_api(): self.abort404() content = Wrapped(c.liveupdate_event) return pages.LiveUpdateEventPage(content=content).render() @require_oauth2_scope("read") @base_listing @api_doc( section=api_section.live, uri="/live/{thread}/discussions", supports_rss=True, ) def GET_discussions(self, num, after, reverse, count): """Get a list of reddit submissions linking to this thread.""" builder = url_links_builder( url="/live/" + c.liveupdate_event._id, num=num, after=after, reverse=reverse, count=count, ) listing = LinkListing(builder).listing() return pages.LiveUpdateEventPage( content=listing, ).render() @validate( VLiveUpdateContributorWithPermission("settings"), ) def GET_edit(self): return pages.LiveUpdateEventPage( content=pages.LiveUpdateEventConfiguration(), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("settings"), VModhash(), **EVENT_CONFIGURATION_VALIDATORS ) @api_doc( section=api_section.live, ) def POST_edit(self, form, jquery, title, description, resources, nsfw): """Configure the thread. Requires the `settings` permission for this thread. See also: [/live/*thread*/about.json](#GET_live_{thread}_about.json). """ if not is_event_configuration_valid(form): return changes = {} if title != c.liveupdate_event.title: changes["title"] = title if description != c.liveupdate_event.description: changes["description"] = description changes["description_html"] = safemarkdown(description, nofollow=True) or "" if resources != c.liveupdate_event.resources: changes["resources"] = resources changes["resources_html"] = safemarkdown(resources, nofollow=True) or "" if nsfw != c.liveupdate_event.nsfw: changes["nsfw"] = nsfw _broadcast(type="settings", payload=changes) c.liveupdate_event.title = title c.liveupdate_event.description = description c.liveupdate_event.resources = resources c.liveupdate_event.nsfw = nsfw c.liveupdate_event._commit() form.set_html(".status", _("saved")) form.refresh() # TODO: pass listing params on @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/contributors", ) def GET_contributors(self): """Get a list of users that contribute to this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor), and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ editable = c.liveupdate_permissions.allow("manage") content = [pages.LinkBackToLiveUpdate()] contributors = c.liveupdate_event.contributors invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) contributor_builder = LiveUpdateContributorBuilder( c.liveupdate_event, contributors, editable) contributor_listing = pages.LiveUpdateContributorListing( c.liveupdate_event, contributor_builder, has_invite=c.user_is_loggedin and c.user._id in invites, is_contributor=c.user_is_loggedin and c.user._id in contributors, ).listing() content.append(contributor_listing) if editable: invite_builder = LiveUpdateInvitedContributorBuilder( c.liveupdate_event, invites, editable) invite_listing = pages.LiveUpdateInvitedContributorListing( c.liveupdate_event, invite_builder, editable=editable, ).listing() content.append(invite_listing) return pages.LiveUpdateEventPage( content=PaneStack(content), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_invite_contributor(self, form, jquery, user, type_and_perms): """Invite another user to contribute to the thread. Requires the `manage` permission for this thread. If the recipient accepts the invite, they will be granted the permissions specified. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/rm_contributor_invite] (#POST_api_live_{thread}_rm_contributor_invite). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) if user._id in invites or user._id in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_ALREADY_CONTRIBUTOR, field="name") form.has_errors("name", errors.LIVEUPDATE_ALREADY_CONTRIBUTOR) return if len(invites) >= g.liveupdate_invite_quota: c.errors.add(errors.LIVEUPDATE_TOO_MANY_INVITES, field="name") form.has_errors("name", errors.LIVEUPDATE_TOO_MANY_INVITES) return LiveUpdateContributorInvitesByEvent.create( c.liveupdate_event, user, permissions) # TODO: make this i18n-friendly when we have such a system for PMs send_system_message( user, subject="invitation to contribute to " + c.liveupdate_event.title, body=INVITE_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, }, ) # add the user to the table contributor = LiveUpdateContributor(user, permissions) user_row = pages.InvitedLiveUpdateContributorTableItem( contributor, c.liveupdate_event, editable=True) jquery(".liveupdate_contributor_invite-table").show( ).find("table").insert_table_rows(user_row) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_leave_contributor(self, form, jquery): """Abdicate contributorship of the thread. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(c.user) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor_invite(self, form, jquery, user): """Revoke an outstanding contributor invite. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, user) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_accept_contributor_invite(self, form, jquery): """Accept a pending invitation to contribute to the thread. See also: [/api/live/*thread*/leave_contributor] (#POST_api_live_{thread}_leave_contributor). """ try: permissions = LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, c.user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND) form.set_error(errors.LIVEUPDATE_NO_INVITE_FOUND, None) return LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, c.user) c.liveupdate_event.add_contributor(c.user, permissions) jquery.refresh() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_set_contributor_permissions(self, form, jquery, user, type_and_perms): """Change a contributor or contributor invite's permissions. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor) and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms if type == "liveupdate_contributor": if user._id not in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_NOT_CONTRIBUTOR, field="user") form.has_errors("user", errors.LIVEUPDATE_NOT_CONTRIBUTOR) return c.liveupdate_event.update_contributor_permissions(user, permissions) elif type == "liveupdate_contributor_invite": try: LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND, field="user") form.has_errors("user", errors.LIVEUPDATE_NO_INVITE_FOUND) return else: LiveUpdateContributorInvitesByEvent.update_invite_permissions( c.liveupdate_event, user, permissions) row = form.closest("tr") editor = row.find(".permissions").data("PermissionEditor") editor.onCommit(permissions.dumps()) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor(self, form, jquery, user): """Revoke another user's contributorship. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(user) @require_oauth2_scope("submit") @validatedForm( VLiveUpdateContributorWithPermission("update"), VModhash(), text=VMarkdownLength("body", max_length=4096), ) @api_doc( section=api_section.live, ) def POST_update(self, form, jquery, text): """Post an update to the thread. Requires the `update` permission for this thread. See also: [/api/live/*thread*/strike_update] (#POST_api_live_{thread}_strike_update), and [/api/live/*thread*/delete_update] (#POST_api_live_{thread}_delete_update). """ if form.has_errors("body", errors.NO_TEXT, errors.TOO_LONG): return # create and store the new update update = LiveUpdate(data={ "author_id": c.user._id, "body": text, "_spam": c.user._spam, }) hooks.get_hook("liveupdate.update").call(update=update) LiveUpdateStream.add_update(c.liveupdate_event, update) # tell the world about our new update builder = LiveUpdateBuilder(None) wrapped = builder.wrap_items([update])[0] rendered = wrapped.render(style="api") _broadcast(type="update", payload=rendered) # Queue up parsing any embeds queue_parse_embeds(c.liveupdate_event, update) # reset the submission form t = form.find("textarea") t.attr('rows', 3).html("").val("") @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_delete_update(self, form, jquery, update): """Delete an update from the thread. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.deleted = True LiveUpdateStream.add_update(c.liveupdate_event, update) _broadcast(type="delete", payload=update._fullname) @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_strike_update(self, form, jquery, update): """Strike (mark incorrect and cross out) the content of an update. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.stricken = True LiveUpdateStream.add_update(c.liveupdate_event, update) _broadcast(type="strike", payload=update._fullname) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("close"), VModhash(), ) @api_doc( section=api_section.live, ) def POST_close_thread(self, form, jquery): """Permanently close the thread, disallowing future updates. Requires the `close` permission for this thread. """ c.liveupdate_event.state = "complete" c.liveupdate_event._commit() queries.complete_event(c.liveupdate_event) _broadcast(type="complete", payload={}) form.refresh() @require_oauth2_scope("report") @validatedForm( VUser(), VModhash(), report_type=VOneOf("type", pages.REPORT_TYPES), ) @api_doc( section=api_section.live, ) def POST_report(self, form, jquery, report_type): """Report the thread for violating the rules of reddit.""" if form.has_errors("type", errors.INVALID_OPTION): return if c.user._spam or c.user.ignorereports: return already_reported = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) if already_reported: self.abort403() LiveUpdateReportsByAccount.create( c.user, c.liveupdate_event, type=report_type) queries.report_event(c.liveupdate_event) try: default_subreddit = Subreddit._by_name(g.default_sr) except NotFound: pass else: not_yet_reported = g.cache.add( "lu_reported_" + str(c.liveupdate_event._id), 1, time=3600) if not_yet_reported: send_system_message( default_subreddit, subject="live thread reported", body=REPORTED_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, "reason": pages.REPORT_TYPES[report_type], }, ) @validatedForm( VAdmin(), VModhash(), ) def POST_approve(self, form, jquery): c.liveupdate_event.banned = False c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event) @validatedForm( VAdmin(), VModhash(), ) def POST_ban(self, form, jquery): c.liveupdate_event.banned = True c.liveupdate_event.banned_by = c.user.name c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event)
class WikiController(RedditController): allow_stylesheets = True @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/{page}', uses_site=True) @validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'), required=False, restricted=False, allow_hidden_revision=False), page_name=VWikiPageName('page', error_on_name_normalized=True)) def GET_wiki_page(self, pv, page_name): """Return the content of a wiki page If `v` is given, show the wiki page as it was at that version If both `v` and `v2` are given, show a diff of the two """ message = None if c.errors.get(('PAGE_NAME_NORMALIZED', 'page')): url = join_urls(c.wiki_base_url, page_name) return self.redirect(url) page, version, version2 = pv if not page: is_api = c.render_style in extensions.API_TYPES if this_may_revise(): if is_api: self.handle_error(404, 'PAGE_NOT_CREATED') errorpage = WikiNotFound(page=page_name) request.environ['usable_error_content'] = errorpage.render() elif is_api: self.handle_error(404, 'PAGE_NOT_FOUND') self.abort404() if version: edit_by = version.get_author() edit_date = version.date else: edit_by = page.get_author() edit_date = page._get('last_edit_date') diffcontent = None if not version: content = page.content if c.is_wiki_mod and page.name in page_descriptions: message = page_descriptions[page.name] else: message = _("viewing revision from %s") % timesince(version.date) if version2: t1 = timesince(version.date) t2 = timesince(version2.date) timestamp1 = _("%s ago") % t1 timestamp2 = _("%s ago") % t2 message = _("comparing revisions from %(date_1)s and %(date_2)s") \ % {'date_1': t1, 'date_2': t2} diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2) content = version2.content else: message = _("viewing revision from %s ago") % timesince( version.date) content = version.content renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') return WikiPageView(content, alert=message, v=version, diff=diffcontent, may_revise=this_may_revise(page), edit_by=edit_by, edit_date=edit_date, page=page.name, renderer=renderer).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions/{page}', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') @validate(page=VWikiPage(('page'), restricted=False)) def GET_wiki_revisions(self, num, after, reverse, count, page): """Retrieve a list of revisions of this wiki `page`""" revisions = page.get_revisions() wikiuser = c.user if c.user_is_loggedin else None builder = WikiRevisionBuilder(revisions, user=wikiuser, sr=c.site, num=num, reverse=reverse, count=count, after=after, skip=not c.is_wiki_mod, wrap=default_thing_wrapper(), page=page) listing = WikiRevisionListing(builder).listing() return WikiRevisions(listing, page=page.name, may_revise=this_may_revise(page)).render() @validate(wp=VWikiPageRevise('page'), page=VWikiPageName('page')) def GET_wiki_create(self, wp, page): api = c.render_style in extensions.API_TYPES error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: error = error.msg_params if wp[0]: VNotInTimeout().run(action_name="wikirevise", details_text="create", target=page) return self.redirect(join_urls(c.wiki_base_url, wp[0].name)) elif api: if error: self.handle_error(403, **error) else: self.handle_error(404, 'PAGE_NOT_CREATED') elif error: error_msg = '' if error['reason'] == 'PAGE_NAME_LENGTH': error_msg = _( "this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters" ) % error['max_length'] elif error['reason'] == 'PAGE_CREATED_ELSEWHERE': error_msg = _( "this page is a special page, please go into the subreddit settings and save the field once to create this special page" ) elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS': error_msg = _( 'a max of %d separators "/" are allowed in a wiki page name.' ) % error['max_separators'] return BoringPage(_("Wiki error"), infotext=error_msg).render() else: VNotInTimeout().run(action_name="wikirevise", details_text="create") return WikiCreate(page=page, may_revise=True).render() @validate(wp=VWikiPageRevise('page', restricted=True, required=True)) def GET_wiki_revise(self, wp, page, message=None, **kw): wp = wp[0] VNotInTimeout().run(action_name="wikirevise", details_text="revise", target=wp) error = c.errors.get(('MAY_NOT_REVISE', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) previous = kw.get('previous', wp._get('revision')) content = kw.get('content', wp.content) if not message and wp.name in page_descriptions: message = page_descriptions[wp.name] return WikiEdit(content, previous, alert=message, page=wp.name, may_revise=True).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') def GET_wiki_recent(self, num, after, reverse, count): """Retrieve a list of recently changed wiki pages in this subreddit""" revisions = WikiRevision.get_recent(c.site) wikiuser = c.user if c.user_is_loggedin else None builder = WikiRecentRevisionBuilder(revisions, num=num, count=count, reverse=reverse, after=after, wrap=default_thing_wrapper(), skip=not c.is_wiki_mod, user=wikiuser, sr=c.site) listing = WikiRevisionListing(builder).listing() return WikiRecent(listing, sr_path=not c.site.is_homepage).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/pages', uses_site=True) def GET_wiki_listing(self): """Retrieve a list of wiki pages in this subreddit""" def check_hidden(page): return page.listed and this_may_view(page) pages, linear_pages = WikiPage.get_listing(c.site, filter_check=check_hidden) return WikiListing(pages, linear_pages, sr_path=not c.site.is_homepage).render() def GET_wiki_redirect(self, page='index'): return self.redirect(str("%s/%s" % (c.wiki_base_url, page)), code=301) @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/discussions/{page}', uses_site=True) @base_listing @validate(page=VWikiPage('page', restricted=True)) def GET_wiki_discussions(self, page, num, after, reverse, count): """Retrieve a list of discussions about this wiki `page`""" page_url = add_sr("%s/%s" % (c.wiki_base_url, page.name)) builder = url_links_builder(page_url, num=num, after=after, reverse=reverse, count=count) listing = LinkListing(builder).listing() return WikiDiscussions(listing, page=page.name, may_revise=this_may_revise(page), sr_path=not c.site.is_homepage).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate(page=VWikiPage('page', restricted=True, modonly=True)) def GET_wiki_settings(self, page): """Retrieve the current permission settings for `page`""" settings = { 'permlevel': page._get('permlevel', 0), 'listed': page.listed } VNotInTimeout().run(action_name="pageview", details_text="wikisettings", target=page) mayedit = page.get_editor_accounts() restricted = (not page.special) and page.restricted show_editors = not restricted return WikiSettings(settings, mayedit, show_settings=not page.special, page=page.name, show_editors=show_editors, restricted=restricted, may_revise=True).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate( VModhash(), page=VWikiPage('page', restricted=True, modonly=True), permlevel=VInt('permlevel'), listed=VBoolean('listed'), ) def POST_wiki_settings(self, page, permlevel, listed): """Update the permissions and visibility of wiki `page`""" oldpermlevel = page.permlevel if oldpermlevel != permlevel: VNotInTimeout().run(action_name="wikipermlevel", details_text="edit", target=page) if page.listed != listed: VNotInTimeout().run(action_name="wikipagelisted", details_text="edit", target=page) try: page.change_permlevel(permlevel) except ValueError: self.handle_error(403, 'INVALID_PERMLEVEL') if page.listed != listed: page.listed = listed page._commit() verb = 'Relisted' if listed else 'Delisted' description = '%s page %s' % (verb, page.name) ModAction.create(c.site, c.user, 'wikipagelisted', description=description) if oldpermlevel != permlevel: description = 'Page: %s, Changed from %s to %s' % ( page.name, oldpermlevel, permlevel) ModAction.create(c.site, c.user, 'wikipermlevel', description=description) return self.GET_wiki_settings(page=page.name) def on_validation_error(self, error): RedditController.on_validation_error(self, error) if error.code: self.handle_error(error.code, error.name) def handle_error(self, code, reason=None, **data): abort(reddit_http_error(code, reason, **data)) def pre(self): RedditController.pre(self) if g.disable_wiki and not c.user_is_admin: self.handle_error(403, 'WIKI_DOWN') if not c.site._should_wiki: self.handle_error(404, 'NOT_WIKIABLE') # /r/mod for an example frontpage = c.site.is_homepage c.wiki_base_url = join_urls(c.site.path, 'wiki') c.wiki_api_url = join_urls(c.site.path, '/api/wiki') # CUSTOM if frontpage: c.wiki_base_url = join_urls('/', 'wiki') c.wiki_api_url = join_urls('/', '/api/wiki') c.wiki_id = g.default_sr if frontpage else c.site.name self.editconflict = False c.is_wiki_mod = (c.user_is_admin or c.site.is_moderator_with_perms( c.user, 'wiki')) if c.user_is_loggedin else False c.wikidisabled = False mode = c.site.wikimode if not mode or mode == 'disabled': if not c.is_wiki_mod: self.handle_error(403, 'WIKI_DISABLED') else: c.wikidisabled = True # Redirects from the old wiki def GET_faq(self): return self.GET_wiki_redirect(page='faq') GET_help = GET_wiki_redirect
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) elif sr == Frontpage or c.user_is_sponsor: available = inventory.get_available_pageviews_geotargeted( sr, location, start, end, datestr=True) else: return abort(403, 'forbidden') 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 # 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 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 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_html(".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_html(".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 or c.user.trusted_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 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 OAuth2FrontendController(RedditController): def check_for_bearer_token(self): pass def pre(self): RedditController.pre(self) require_https() def _abort_oauth_error(self, error): g.stats.simple_event('oauth2.errors.%s' % error) abort(BadRequestError(error)) def _check_redirect_uri(self, client, redirect_uri): if (errors.OAUTH2_INVALID_CLIENT, 'client_id') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_CLIENT) if not redirect_uri or redirect_uri != client.redirect_uri: self._abort_oauth_error(errors.OAUTH2_INVALID_REDIRECT_URI) def _check_response_type_and_scope(self, response_type, scope): if (errors.INVALID_OPTION, 'response_type') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_RESPONSE_TYPE) if (errors.OAUTH2_INVALID_SCOPE, 'scope') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_SCOPE) def _check_client_type_and_duration(self, response_type, client, duration): if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner self._abort_oauth_error(errors.OAUTH2_CONFIDENTIAL_TOKEN) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed self._abort_oauth_error(errors.OAUTH2_NO_REFRESH_TOKENS_ALLOWED) def _error_response(self, state, redirect_uri, as_fragment=False): """Return an error redirect.""" resp = {"state": state} if (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors: resp["error"] = "access_denied" elif (errors.INVALID_MODHASH, None) in c.errors: resp["error"] = "access_denied" else: resp["error"] = "invalid_request" final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment) return self.redirect(final_redirect, code=302) def _check_employee_grants(self, client, scope): if not c.user.employee or not client or not scope: return if client._id in g.employee_approved_clients: return if client._id in g.mobile_auth_allowed_clients: return # The identity scope doesn't leak much, and we don't mind if employees # prove their identity to some external service if scope.scopes == {"identity"}: return error_page = RedditError( title=_( 'this app has not been approved for use with employee accounts' ), message="", ) request.environ["usable_error_content"] = error_page.render() self.abort403() @validate(VUser(), response_type=VOneOf("response_type", ("code", "token")), client=VOAuth2ClientID(), redirect_uri=VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope=VOAuth2Scope(), state=VRequired("state", errors.NO_TEXT), duration=VOneOf("duration", ("temporary", "permanent"), default="temporary")) def GET_authorize(self, response_type, client, redirect_uri, scope, state, duration): """ First step in [OAuth 2.0](http://oauth.net/2/) authentication. End users will be prompted for their credentials (username/password) and asked if they wish to authorize the application identified by the **client_id** parameter with the permissions specified by the **scope** parameter. They are then redirected to the endpoint on the client application's side specified by **redirect_uri**. If the user granted permission to the application, the response will contain a **code** parameter with a temporary authorization code which can be exchanged for an access token at [/api/v1/access_token](#api_method_access_token). **redirect_uri** must match the URI configured for the client in the [app preferences](/prefs/apps). All errors will show a 400 error page along with some information on what option was wrong. """ self._check_employee_grants(client, scope) # Check redirect URI first; it will ensure client exists self._check_redirect_uri(client, redirect_uri) self._check_response_type_and_scope(response_type, scope) self._check_client_type_and_duration(response_type, client, duration) if not c.errors: return OAuth2AuthorizationPage(client, redirect_uri, scope, state, duration, response_type).render() else: self._abort_oauth_error(errors.INVALID_OPTION) @validate(VUser(), VModhash(fatal=False), client=VOAuth2ClientID(), redirect_uri=VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope=VOAuth2Scope(), state=VRequired("state", errors.NO_TEXT), duration=VOneOf("duration", ("temporary", "permanent"), default="temporary"), authorize=VRequired("authorize", errors.OAUTH2_ACCESS_DENIED), response_type=VOneOf("response_type", ("code", "token"), default="code")) def POST_authorize(self, authorize, client, redirect_uri, scope, state, duration, response_type): """Endpoint for OAuth2 authorization.""" self._check_employee_grants(client, scope) self._check_redirect_uri(client, redirect_uri) self._check_response_type_and_scope(response_type, scope) self._check_client_type_and_duration(response_type, client, duration) if c.errors: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) if response_type == "code": code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id36, scope, duration == "permanent") resp = {"code": code._id, "state": state} final_redirect = _update_redirect_uri(redirect_uri, resp) g.stats.simple_event( 'oauth2.POST_authorize.authorization_code_create') elif response_type == "token": device_id = get_device_id(client) token = OAuth2AccessToken._new( client_id=client._id, user_id=c.user._id36, scope=scope, device_id=device_id, ) resp = OAuth2AccessController._make_new_token_response(token) resp["state"] = state final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment=True) g.stats.simple_event('oauth2.POST_authorize.access_token_create') # If this is the first time the user is logging in with an official # mobile app, gild them if (g.live_config.get('mobile_gild_first_login') and not c.user.has_used_mobile_app and client._id in g.mobile_auth_gild_clients): buyer = Account.system_user() admintools.adjust_gold_expiration(c.user, days=g.mobile_auth_gild_time) create_gift_gold(buyer._id, c.user._id, g.mobile_auth_gild_time, datetime.now(g.tz), signed=True, note='first_mobile_auth') subject = 'Let there be gold! Reddit just sent you Reddit gold!' message = ( "Thank you for using the Reddit mobile app! As a thank you " "for logging in during launch week, you've been gifted %s of " "Reddit Gold.\n\n" "Reddit Gold is Reddit's premium membership program, which " "grants you: \n" "An ads-free experience in Reddit's mobile apps, and\n" "Extra site features on desktop\n\n" "Discuss and get help on the features and perks at " + g.brander_community_abbr + "/goldbenefits.") % g.mobile_auth_gild_message message += '\n\n' + strings.gold_benefits_msg send_system_message(c.user, subject, message, add_to_sent=False) c.user.has_used_mobile_app = True c.user._commit() return self.redirect(final_redirect, code=302)
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 c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) VNotInTimeout().run(action_name="wikirevise", details_text="create") page = WikiPage.create(c.site, page_name) else: VNotInTimeout().run(action_name="wikirevise", details_text="edit", target=page) error = c.errors.get(('MAY_NOT_REVISE', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) 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': VNotInTimeout().run(action_name="wikipermlevel", details_text="del_editor", target=user) page.remove_editor(user._id36) elif act == 'add': VNotInTimeout().run(action_name="wikipermlevel", details_text="allow_editor", target=user) 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') VNotInTimeout().run(action_name="wikirevise", details_text="revision_hide", target=page) 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') VNotInTimeout().run(action_name="wikirevise", details_text="revision_revert", target=page) 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 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 @classmethod @memoize('house_campaigns', time=60) def get_house_campaigns(cls): now = promote.promo_datetime_now() pws = PromotionWeights.get_campaigns(now) campaign_ids = {pw.promo_idx for pw in pws} campaigns = PromoCampaign._byID(campaign_ids, data=True, return_dict=False) campaigns = [camp for camp in campaigns if not camp.priority.cpm] return campaigns @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] elif self.sort == 'reported': return queries.get_reported_links(get_promote_srid()) elif self.sort == 'house': campaigns = self.get_house_campaigns() 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: # never reached--see MinimalController.on_validation_error 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)) @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) 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 = 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_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), username=VLength('username', 100, empty_error=None), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), selftext=VSelfText('text'), kind=VOneOf('kind', ['link', 'self']), 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, username, l, title, url, selftext, kind, 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 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 and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) # selftext can be changed at any time if kind == 'self': l.selftext = selftext # 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'], 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_id'), VModhash(), dates=VDateRange( ['startdate', 'enddate'], earliest=timedelta(days=1), latest=timedelta(days=g.max_promote_future), reference_date=promote.promo_datetime_now, business_days=True, 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), priority=VPriority("priority")) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, sr, targeting, priority): 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(): day = promote.promo_datetime_now(offset=g.min_promote_future) day = day.strftime("%m/%d/%Y") c.errors.add(errors.DATE_TOO_EARLY, msg_params=dict(day=day), field="startdate") 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 priority.cpm: if 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 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 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 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_id = campaign._id if campaign else None if (not priority.inventory_override and has_oversold_error( form, campaign_id, start, end, bid, cpm, sr)): return if campaign: promote.edit_campaign(link, campaign, dates, bid, cpm, sr, priority) 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.priority_name, r.inventory_override, r.status) else: campaign = promote.new_campaign(link, dates, bid, cpm, sr, priority) 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.priority_name, r.inventory_override, 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" ]), 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=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() @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 GoldApiController(RedditController): @validate( VUser(), VGold(), ) def GET_snoovatar(self): snoovatar = SnoovatarsByAccount.load(c.user, "snoo") response.content_type = "application/json" return json.dumps(snoovatar) @validatedForm( VUser(), VGold(), VModhash(), public=VBoolean("public"), snoo_color=VSnooColor("snoo_color"), unvalidated_components=VJSON("components"), ) def POST_snoovatar(self, form, jquery, public, snoo_color, unvalidated_components): if form.has_errors( "components", errors.NO_TEXT, errors.TOO_LONG, errors.BAD_STRING, ): return if form.has_errors("snoo_color", errors.BAD_CSS_COLOR): return try: tailors = g.plugins["gold"].tailors_data validated = {} for tailor in tailors: tailor_name = tailor["name"] component = unvalidated_components.get(tailor_name) # if the tailor requires a selection, ensure there is one if not tailor["allow_clear"]: require(component) # ensure this dressing exists dressing = component.get("dressingName") if dressing: for d in tailor["dressings"]: if dressing == d["name"]: break else: raise RequirementException validated[tailor_name] = component except RequirementException: c.errors.add(errors.INVALID_SNOOVATAR, field="components") form.has_errors("components", errors.INVALID_SNOOVATAR) return SnoovatarsByAccount.save( user=c.user, name="snoo", public=public, snoo_color=snoo_color, components=validated, )
class MultiApiController(RedditController): on_validation_error = staticmethod(abort_with_error) def pre(self): set_extension(request.environ, "json") RedditController.pre(self) def _format_multi_list(self, multis, viewer, expand_srs): templ = LabeledMultiJsonTemplate(expand_srs) resp = [ templ.render(multi).finalize() for multi in multis if multi.can_view(viewer) ] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( user=VAccountByName("username"), expand_srs=VBoolean("expand_srs"), ) @api_doc(api_section.multis, uri="/api/multi/user/{username}") def GET_list_multis(self, user, expand_srs): """Fetch a list of public multis belonging to `username`""" multis = LabeledMulti.by_owner(user) return self._format_multi_list(multis, c.user, expand_srs) @require_oauth2_scope("read") @validate(VUser(), expand_srs=VBoolean("expand_srs")) @api_doc(api_section.multis, uri="/api/multi/mine") def GET_my_multis(self, expand_srs): """Fetch a list of multis belonging to the current user.""" multis = LabeledMulti.by_owner(c.user) return self._format_multi_list(multis, c.user, expand_srs) def _format_multi(self, multi, expand_sr_info=False): multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi) return self.api_wrapper(multi_info.finalize()) @require_oauth2_scope("read") @validate( multi=VMultiByPath("multipath", require_view=True), expand_srs=VBoolean("expand_srs"), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}", uri_variants=['/api/filter/{filterpath}'], ) def GET_multi(self, multi, expand_srs): """Fetch a multi's data and subreddit list by name.""" return self._format_multi(multi, expand_srs) def _check_new_multi_path(self, path_info): if path_info['owner'].lower() != c.user.name.lower(): raise RedditError('MULTI_CANNOT_EDIT', code=403, fields='multipath') return c.user def _add_multi_srs(self, multi, sr_datas): srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas) for sr in srs.itervalues(): if isinstance(sr, FakeSubreddit): raise RedditError('MULTI_SPECIAL_SUBREDDIT', msg_params={'path': sr.path}, code=400) sr_props = {} for sr_data in sr_datas: try: sr = srs[sr_data['name']] except KeyError: raise RedditError('SUBREDDIT_NOEXIST', code=400) else: # name is passed in via the API data format, but should not be # stored on the model. del sr_data['name'] sr_props[sr] = sr_data try: multi.add_srs(sr_props) except TooManySubredditsError as e: raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409) return sr_props def _write_multi_data(self, multi, data): srs = data.pop('subreddits', None) if srs is not None: multi.clear_srs() try: self._add_multi_srs(multi, srs) except: multi._revert() raise if 'icon_name' in data: try: multi.set_icon_by_name(data.pop('icon_name')) except: multi._revert() raise for key, val in data.iteritems(): if key in WRITABLE_MULTI_FIELDS: setattr(multi, key, val) multi._commit() return multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath", required=False), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def POST_multi(self, path_info, data): """Create a multi. Responds with 409 Conflict if it already exists.""" if not path_info and "path" in data: path_info = VMultiPath("").run(data["path"]) elif 'display_name' in data: # if path not provided, create multi for user path = LabeledMulti.slugify(c.user, data['display_name']) path_info = VMultiPath("").run(path) if not path_info: raise RedditError('BAD_MULTI_PATH', code=400) owner = self._check_new_multi_path(path_info) try: LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], owner) response.status = 201 else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def PUT_multi(self, path_info, data): """Create or update a multi.""" owner = self._check_new_multi_path(path_info) try: multi = LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], owner) response.status = 201 self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), ) @api_doc(api_section.multis, extends=GET_multi) def DELETE_multi(self, multi): """Delete a multi.""" multi.delete() def _copy_multi(self, from_multi, to_path_info, rename=False): """Copy a multi to a user account.""" to_owner = self._check_new_multi_path(to_path_info) # rename requires same owner if rename and from_multi.owner != to_owner: raise RedditError('MULTI_CANNOT_EDIT', code=400) try: LabeledMulti._byID(to_path_info['path']) except tdb_cassandra.NotFound: to_multi = LabeledMulti.copy(to_path_info['path'], from_multi, owner=to_owner) else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') return to_multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_view=True, kinds='m'), to_path_info=VMultiPath( "to", required=False, docs={"to": "destination multireddit url path"}, ), display_name=VLength("display_name", max_length=MAX_DISP_NAME, empty_error=None), ) @api_doc( api_section.multis, uri="/api/multi/copy", ) def POST_multi_copy(self, from_multi, to_path_info, display_name): """Copy a multi. Responds with 409 Conflict if the target already exists. A "copied from ..." line will automatically be appended to the description. """ if not to_path_info: if display_name: # if path not provided, copy multi to same owner path = LabeledMulti.slugify(from_multi.owner, display_name) to_path_info = VMultiPath("").run(path) else: raise RedditError('BAD_MULTI_PATH', code=400) to_multi = self._copy_multi(from_multi, to_path_info) from_path = from_multi.path to_multi.copied_from = from_path if to_multi.description_md: to_multi.description_md += '\n\n' to_multi.description_md += _('copied from %(source)s') % { # force markdown linking since /user/foo is not autolinked 'source': '[%s](%s)' % (from_path, from_path) } to_multi.visibility = 'private' if display_name: to_multi.display_name = display_name to_multi._commit() return self._format_multi(to_multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_edit=True, kinds='m'), to_path_info=VMultiPath( "to", required=False, docs={"to": "destination multireddit url path"}, ), display_name=VLength("display_name", max_length=MAX_DISP_NAME, empty_error=None), ) @api_doc( api_section.multis, uri="/api/multi/rename", ) def POST_multi_rename(self, from_multi, to_path_info, display_name): """Rename a multi.""" if not to_path_info: if display_name: path = LabeledMulti.slugify(from_multi.owner, display_name) to_path_info = VMultiPath("").run(path) else: raise RedditError('BAD_MULTI_PATH', code=400) to_multi = self._copy_multi(from_multi, to_path_info, rename=True) if display_name: to_multi.display_name = display_name to_multi._commit() from_multi.delete() return self._format_multi(to_multi) def _get_multi_subreddit(self, multi, sr): resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True), sr=VSRByName('srname'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/r/{srname}", uri_variants=['/api/filter/{filterpath}/r/{srname}'], ) def GET_multi_subreddit(self, multi, sr): """Get data about a subreddit in a multi.""" return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr_name=VSubredditName('srname', allow_language_srs=True), data=VValidatedJSON("model", multi_sr_data_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def PUT_multi_subreddit(self, multi, sr_name, data): """Add a subreddit to a multi.""" new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs) data['name'] = sr_name sr_props = self._add_multi_srs(multi, [data]) sr = sr_props.items()[0][0] multi._commit() if new: response.status = 201 return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr=VSRByName('srname'), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def DELETE_multi_subreddit(self, multi, sr): """Remove a subreddit from a multi.""" multi.del_srs(sr) multi._commit() def _format_multi_description(self, multi): resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True, kinds='m'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/description", ) def GET_multi_description(self, multi): """Get a multi's description.""" return self._format_multi_description(multi) @require_oauth2_scope("read") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True, kinds='m'), data=VValidatedJSON('model', multi_description_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_description) def PUT_multi_description(self, multi, data): """Change a multi's markdown description.""" multi.description_md = data['body_md'] multi._commit() return self._format_multi_description(multi)
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( format_html("<p>%s</p>", text)) @validatedForm( VSponsorAdmin(), VModhash(), thing=VByName("thing_id"), is_fraud=VBoolean("fraud"), ) def POST_review_fraud(self, form, jquery, thing, is_fraud): if not promote.is_promo(thing): return promote.review_fraud(thing, is_fraud) button = jquery(".id-%s .fraud-button" % thing._fullname) button.text(_("fraud" if is_fraud else "not fraud")) form.fadeOut() @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(c.user, 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, data=True) except NotFound: pass if campaign and (campaign._deleted or link._id != campaign.link_id): campaign = None if not campaign: 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) if pay_id: promote.new_payment_method(user=c.user, ip=request.ip, address=address, link=link) if pay_id: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) if success: form.redirect(promote.promo_edit_url(link)) return else: promote.failed_payment_method(c.user, link) msg = reason or _("failed to authenticate card. sorry.") form.set_text(".status", msg) else: promote.failed_payment_method(c.user, link) form.set_text(".status", _("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): 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 WikiController(RedditController): allow_stylesheets = True @validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'), required=False, restricted=False, allow_hidden_revision=False), page_name=VWikiPageName('page', error_on_name_normalized=True)) def GET_wiki_page(self, pv, page_name): message = None if c.errors.get(('PAGE_NAME_NORMALIZED', 'page')): url = join_urls(c.wiki_base_url, page_name) return self.redirect(url) page, version, version2 = pv if not page: is_api = c.render_style in extensions.API_TYPES if this_may_revise(): if is_api: self.handle_error(404, 'PAGE_NOT_CREATED') errorpage = WikiNotFound(page=page_name) request.environ['usable_error_content'] = errorpage.render() elif is_api: self.handle_error(404, 'PAGE_NOT_FOUND') self.abort404() if version: edit_by = version.get_author() edit_date = version.date else: edit_by = page.get_author() edit_date = page._get('last_edit_date') diffcontent = None if not version: content = page.content if c.is_wiki_mod and page.name in page_descriptions: message = page_descriptions[page.name] else: message = _("viewing revision from %s") % timesince(version.date) if version2: t1 = timesince(version.date) t2 = timesince(version2.date) timestamp1 = _("%s ago") % t1 timestamp2 = _("%s ago") % t2 message = _("comparing revisions from %(date_1)s and %(date_2)s") \ % {'date_1': t1, 'date_2': t2} diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2) content = version2.content else: message = _("viewing revision from %s ago") % timesince(version.date) content = version.content return WikiPageView(content, alert=message, v=version, diff=diffcontent, may_revise=this_may_revise(page), edit_by=edit_by, edit_date=edit_date, page=page.name).render() @paginated_listing(max_page_size=100, backend='cassandra') @validate(page=VWikiPage(('page'), restricted=False)) def GET_wiki_revisions(self, num, after, reverse, count, page): revisions = page.get_revisions() builder = WikiRevisionBuilder(revisions, num=num, reverse=reverse, count=count, after=after, skip=not c.is_wiki_mod, wrap=default_thing_wrapper()) listing = WikiRevisionListing(builder).listing() return WikiRevisions(listing, page=page.name, may_revise=this_may_revise(page)).render() @validate(wp=VWikiPageRevise('page'), page=VWikiPageName('page')) def GET_wiki_create(self, wp, page): api = c.render_style in extensions.API_TYPES error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: error = error.msg_params if wp[0]: return self.redirect(join_urls(c.wiki_base_url, wp[0].name)) elif api: if error: self.handle_error(403, **error) else: self.handle_error(404, 'PAGE_NOT_CREATED') elif error: error_msg = '' if error['reason'] == 'PAGE_NAME_LENGTH': error_msg = _("this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters") % error['max_length'] elif error['reason'] == 'PAGE_CREATED_ELSEWHERE': error_msg = _("this page is a special page, please go into the subreddit settings and save the field once to create this special page") elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS': error_msg = _('a max of %d separators "/" are allowed in a wiki page name.') % error['max_separators'] return BoringPage(_("Wiki error"), infotext=error_msg).render() else: return WikiCreate(page=page, may_revise=True).render() @validate(wp=VWikiPageRevise('page', restricted=True, required=True)) def GET_wiki_revise(self, wp, page, message=None, **kw): wp = wp[0] previous = kw.get('previous', wp._get('revision')) content = kw.get('content', wp.content) if not message and wp.name in page_descriptions: message = page_descriptions[wp.name] return WikiEdit(content, previous, alert=message, page=wp.name, may_revise=True).render() @paginated_listing(max_page_size=100, backend='cassandra') def GET_wiki_recent(self, num, after, reverse, count): revisions = WikiRevision.get_recent(c.site) builder = WikiRecentRevisionBuilder(revisions, num=num, count=count, reverse=reverse, after=after, wrap=default_thing_wrapper(), skip=not c.is_wiki_mod) listing = WikiRevisionListing(builder).listing() return WikiRecent(listing).render() def GET_wiki_listing(self): def check_hidden(page): return this_may_view(page) pages, linear_pages = WikiPage.get_listing(c.site, filter_check=check_hidden) return WikiListing(pages, linear_pages).render() def GET_wiki_redirect(self, page='index'): return redirect_to(str("%s/%s" % (c.wiki_base_url, page)), _code=301) @base_listing @validate(page=VWikiPage('page', restricted=True)) def GET_wiki_discussions(self, page, num, after, reverse, count): page_url = add_sr("%s/%s" % (c.wiki_base_url, page.name)) builder = url_links_builder(page_url) listing = LinkListing(builder).listing() return WikiDiscussions(listing, page=page.name, may_revise=this_may_revise(page)).render() @validate(page=VWikiPage('page', restricted=True, modonly=True)) def GET_wiki_settings(self, page): settings = {'permlevel': page._get('permlevel', 0)} mayedit = page.get_editor_accounts() restricted = (not page.special) and page.restricted show_editors = not restricted return WikiSettings(settings, mayedit, show_settings=not page.special, page=page.name, show_editors=show_editors, restricted=restricted, may_revise=True).render() @validate(VModhash(), page=VWikiPage('page', restricted=True, modonly=True), permlevel=VInt('permlevel')) def POST_wiki_settings(self, page, permlevel): oldpermlevel = page.permlevel try: page.change_permlevel(permlevel) except ValueError: self.handle_error(403, 'INVALID_PERMLEVEL') description = 'Page: %s, Changed from %s to %s' % (page.name, oldpermlevel, permlevel) ModAction.create(c.site, c.user, 'wikipermlevel', description=description) return self.GET_wiki_settings(page=page.name) def on_validation_error(self, error): RedditController.on_validation_error(self, error) if error.code: self.handle_error(error.code, error.name) def handle_error(self, code, reason=None, **data): abort(reddit_http_error(code, reason, **data)) def pre(self): RedditController.pre(self) if g.disable_wiki and not c.user_is_admin: self.handle_error(403, 'WIKI_DOWN') if not c.site._should_wiki: self.handle_error(404, 'NOT_WIKIABLE') # /r/mod for an example frontpage = isinstance(c.site, DefaultSR) c.wiki_base_url = join_urls(c.site.path, 'wiki') c.wiki_api_url = join_urls(c.site.path, '/api/wiki') c.wiki_id = g.default_sr if frontpage else c.site.name self.editconflict = False c.is_wiki_mod = ( c.user_is_admin or c.site.is_moderator_with_perms(c.user, 'wiki') ) if c.user_is_loggedin else False c.wikidisabled = False mode = c.site.wikimode if not mode or mode == 'disabled': if not c.is_wiki_mod: self.handle_error(403, 'WIKI_DISABLED') else: c.wikidisabled = True # Redirects from the old wiki def GET_faq(self): return self.GET_wiki_redirect(page='faq') GET_help = GET_wiki_redirect
class GoldApiController(RedditController): @json_validate(VUser(), VGold(), VModhash(), deal=VLength('deal', 100)) def POST_claim_gold_partner_deal_code(self, responder, deal): try: return {'code': GoldPartnerDealCode.claim_code(c.user, deal)} except GoldPartnerCodesExhaustedError: return { 'error': 'GOLD_PARTNER_CODES_EXHAUSTED', 'explanation': _("sorry, we're out of codes!") } @validatedForm( VUser(), VGold(), VModhash(), public=VBoolean("public"), snoo_color=VSnooColor("snoo_color"), unvalidated_components=VJSON("components"), ) def POST_snoovatar(self, form, jquery, public, snoo_color, unvalidated_components): if not feature.is_enabled('snoovatars'): return if form.has_errors( "components", errors.NO_TEXT, errors.TOO_LONG, errors.BAD_STRING, ): return if form.has_errors("snoo_color", errors.BAD_CSS_COLOR): return try: tailors = g.plugins["gold"].tailors_data validated = {} for tailor in tailors: tailor_name = tailor["name"] component = unvalidated_components.get(tailor_name) # if the tailor requires a selection, ensure there is one if not tailor["allow_clear"]: require(component) # ensure this dressing exists if component: for dressing in tailor["dressings"]: if component == dressing["name"]: break else: raise RequirementException validated[tailor_name] = component except RequirementException: c.errors.add(errors.INVALID_SNOOVATAR, field="components") form.has_errors("components", errors.INVALID_SNOOVATAR) return SnoovatarsByAccount.save( user=c.user, name="snoo", public=public, snoo_color=snoo_color, components=validated, )
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", ) @validate(geo=VCountryCode()) def GET_happening_now(self, geo): """ 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() or not feature.is_enabled('live_happening_now'): self.abort404() featured_event = get_featured_event(geo) if not featured_event: response.status_code = 204 return c.liveupdate_event = featured_event content = Wrapped(featured_event) return pages.LiveUpdateEventPage(content).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 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 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)
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()