Ejemplo n.º 1
0
class SteamController(RedditController):
    @staticmethod
    def make_post_login_url():
        return add_sr("/f2p/steam/postlogin")

    @validate(VUser(), error=nop("error"))
    def GET_start(self, error):
        f2p_status = getattr(c.user, "f2p", None)
        error = bool(error)

        if f2p_status == "participated":
            return SteamPage(content=SteamStart(error=error)).render()
        elif f2p_status == "claiming":
            return SteamPage(content=SteamInProgress()).render()
        elif f2p_status == "claimed":
            return SteamPage(content=SteamStop()).render()
        return SteamPage(content=SteamSorry()).render()

    @validate(VUser(), VModhash())
    def POST_auth(self):
        if getattr(c.user, "f2p", None) != "participated":
            abort(403)

        session = {}
        consumer = openid.consumer.consumer.Consumer(session, store=None)
        auth_request = consumer.begin(STEAM_AUTH_URL)
        post_login_url = self.make_post_login_url()
        url = auth_request.redirectURL(realm=g.origin,
                                       return_to=post_login_url)
        g.f2pcache.set("steam_session_%d" % c.user._id, session)
        g.log.debug("started steam auth for %s", c.user.name)
        return redirect_to(url)

    @validate(VUser())
    def GET_postlogin(self):
        if getattr(c.user, "f2p", None) != "participated":
            return redirect_to("/f2p/steam")

        session = g.f2pcache.get("steam_session_%d" % c.user._id)
        if not session:
            return redirect_to("/f2p/steam?error=no_session")

        consumer = openid.consumer.consumer.Consumer(session, store=None)
        auth_response = consumer.complete(request.params, request.url)

        if auth_response.status == openid.consumer.consumer.CANCEL:
            return redirect_to("/f2p/steam?error=cancel")

        if auth_response.status != openid.consumer.consumer.SUCCESS:
            return redirect_to("/f2p/steam?error=not_success")

        steamid_match = STEAMID_EXTRACTOR.search(auth_response.identity_url)
        if not steamid_match:
            return redirect_to("/f2p/steam?error=id_mismatch")

        steamid = steamid_match.group(1)
        g.log.debug("successful steam auth for %r", steamid)

        with g.make_lock("f2p", "steam_claim_%d" % c.user._id):
            c.user._sync_latest()
            if c.user.f2p != "participated":
                return redirect_to("/f2p/steam")

            c.user.f2p = "claiming"
            c.user._commit()

        message = json.dumps({
            "user-id": c.user._id,
            "steam-id": steamid,
        })
        amqp.add_item(QNAME, message)

        return redirect_to("/f2p/steam")
Ejemplo n.º 2
0
class PoliciesController(RedditController):
    @validate(requested_rev=nop('v'))
    def GET_policy_page(self, page, requested_rev):
        if page == 'privacypolicy':
            wiki_name = g.wiki_page_privacy_policy
            pagename = _('privacy policy')
        elif page == 'useragreement':
            wiki_name = g.wiki_page_user_agreement
            pagename = _('user agreement')
        else:
            abort(404)

        wp = WikiPage.get(Frontpage, wiki_name)

        revs = list(wp.get_revisions())

        # collapse minor edits into revisions with reasons
        rev_info = []
        last_edit = None
        for rev in revs:
            if rev.is_hidden:
                continue

            if not last_edit:
                last_edit = rev

            if rev._get('reason'):
                rev_info.append({
                    'id': str(last_edit._id),
                    'title': rev._get('reason'),
                })
                last_edit = None

        if requested_rev:
            try:
                display_rev = WikiRevision.get(requested_rev, wp._id)
            except (tdb_cassandra.NotFound, ValueError):
                abort(404)
        else:
            display_rev = revs[0]

        doc_html = wikimarkdown(display_rev.content, include_toc=False)
        soup = BeautifulSoup(doc_html.decode('utf-8'))
        toc = generate_table_of_contents(soup, prefix='section')
        self._number_sections(soup)
        self._linkify_headings(soup)

        content = PolicyView(
            body_html=unsafe(soup),
            toc_html=unsafe(toc),
            revs=rev_info,
            display_rev=str(display_rev._id),
        )
        return PolicyPage(
            pagename=pagename,
            content=content,
        ).render()

    def _number_sections(self, soup):
        count = 1
        for para in soup.find('div', 'md').findAll(['p'], recursive=False):
            a = Tag(soup, 'a', [
                ('class', 'p-anchor'),
                ('id', 'p_%d' % count),
                ('href', '#p_%d' % count),
            ])
            a.append(str(count))
            para.insert(0, a)
            para.insert(1, ' ')
            count += 1

    def _linkify_headings(self, soup):
        md_el = soup.find('div', 'md')
        for heading in md_el.findAll(['h1', 'h2', 'h3'], recursive=False):
            heading_a = Tag(soup, "a", [('href', '#%s' % heading['id'])])
            heading_a.contents = heading.contents
            heading.contents = []
            heading.append(heading_a)
Ejemplo n.º 3
0
class PromoteController(RedditController):
    @validate(VSponsor())
    def GET_new_promo(self):
        return PromotePage(title=_("create sponsored link"),
                           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, skip=False)
        form = PromoteLinkEdit(link, rendered)
        page = PromotePage(title=_("edit sponsored link"),
                           content=form,
                           show_sidebar=False,
                           extension_handling=False)
        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(VSponsorAdmin(),
              link=VLink("link"),
              campaign=VPromoCampaign("campaign"))
    def GET_refund(self, link, campaign):
        if link._id != campaign.link_id:
            return self.abort404()

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

    @validate(VSponsorAdmin())
    def GET_roadblock(self):
        return PromotePage(title=_("manage roadblocks"),
                           content=Roadblocks()).render()

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

        # 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 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()

    @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(title=_("sponsored link report"),
                               content=content).render()

    @validate(
        VSponsorAdmin(),
        start=VDate('startdate', reference_date=promote.promo_datetime_now),
        end=VDate('enddate', reference_date=promote.promo_datetime_now),
        sr_name=nop('sr_name'),
    )
    def GET_promote_inventory(self, start, end, sr_name):
        if not start or not end:
            start = promote.promo_datetime_now(offset=1).date()
            end = promote.promo_datetime_now(offset=8).date()
            c.errors.remove((errors.BAD_DATE, 'startdate'))
            c.errors.remove((errors.BAD_DATE, 'enddate'))

        sr = Frontpage
        if sr_name:
            try:
                sr = Subreddit._by_name(sr_name)
            except NotFound:
                c.errors.add(errors.SUBREDDIT_NOEXIST, field='sr_name')

        content = PromoteInventory(start, end, sr)
        return PromotePage(title=_("sponsored link inventory"),
                           content=content).render()
Ejemplo n.º 4
0
class PromoteListingController(ListingController):
    where = 'promoted'
    render_cls = PromotePage
    titles = {
        'future_promos': N_('unapproved promoted links'),
        'pending_promos': N_('accepted promoted links'),
        'unpaid_promos': N_('unpaid promoted links'),
        'rejected_promos': N_('rejected promoted links'),
        'live_promos': N_('live promoted links'),
        'underdelivered': N_('underdelivered promoted links'),
        'reported': N_('reported promoted links'),
        'house': N_('house promoted links'),
        'all': N_('all promoted links'),
    }

    def title(self):
        return _(self.titles[self.sort])

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

    @classmethod
    @memoize('live_by_subreddit', time=300)
    def _live_by_subreddit(cls, sr_names):
        promotuples = promote.get_live_promotions(sr_names)
        return [pt.link for pt in promotuples]

    def live_by_subreddit(cls, sr):
        return cls._live_by_subreddit([sr.name])

    @classmethod
    @memoize('house_link_names', time=60)
    def get_house_link_names(cls):
        now = promote.promo_datetime_now()
        pws = PromotionWeights.get_campaigns(now)
        campaign_ids = {pw.promo_idx for pw in pws}
        q = PromoCampaign._query(PromoCampaign.c._id.in_(campaign_ids),
                                 PromoCampaign.c.priority_name == 'house',
                                 data=True)
        link_names = {
            Link._fullname_from_id36(to36(camp.link_id))
            for camp in q
        }
        return sorted(link_names, reverse=True)

    @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:
            srnames = promote.all_live_promo_srnames()
            buttons = [NavButton('all', '')]
            try:
                srnames.remove(Frontpage.name)
                frontbutton = NavButton('FRONTPAGE',
                                        Frontpage.name,
                                        aliases=[
                                            '/promoted/live_promos/%s' %
                                            urllib.quote(Frontpage.name)
                                        ])
                buttons.append(frontbutton)
            except KeyError:
                pass

            srnames = sorted(srnames, key=lambda name: name.lower())
            buttons.extend([NavButton(name, name) for name in srnames])
            menus.append(
                NavMenu(buttons,
                        base_path='/promoted/live_promos',
                        title='subreddit',
                        type='lightdrop'))

        return menus

    def keep_fn(self):
        def keep(item):
            if self.sort == "future_promos":
                # this sort is used to review links that need to be approved
                # skip links that don't have any paid campaigns
                campaigns = list(PromoCampaign._by_link(item._id))
                if not any(
                        promote.authed_or_not_needed(camp)
                        for camp in campaigns):
                    return False

            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(Subreddit.get_promote_srid())
            elif self.sort == 'house':
                return self.get_house_link_names()
            elif self.sort == 'all':
                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)
            elif self.sort == "all":
                return queries.get_promoted_links(c.user._id)

    @validate(VSponsor(), sr=nop('sr'))
    def GET_listing(self, sr=None, sort="all", **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")

        if (sort in ('underdelivered', 'reported', 'house')
                and not c.user_is_sponsor):
            self.abort403()

        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)
Ejemplo n.º 5
0
Archivo: wiki.py Proyecto: znanl/reddit
class WikiApiController(WikiController):
    @require_oauth2_scope("wikiedit")
    @validate(VModhash(),
              pageandprevious=VWikiPageRevise(('page', 'previous'),
                                              restricted=True),
              content=nop(('content')),
              page_name=VWikiPageName('page'),
              reason=VPrintable('reason', 256, empty_error=None))
    @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True)
    def POST_wiki_edit(self, pageandprevious, content, page_name, reason):
        """Edit a wiki `page`"""
        page, previous = pageandprevious

        if not page:
            error = c.errors.get(('WIKI_CREATE_ERROR', 'page'))
            if error:
                self.handle_error(403, **(error.msg_params or {}))
            if not c.user._spam:
                page = WikiPage.create(c.site, page_name)
        if c.user._spam:
            error = _("You are doing that too much, please try again later.")
            self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error])

        renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki')
        if renderer in ('wiki', 'reddit'):
            content = VMarkdown(('content'), renderer=renderer).run(content)

        # Use the raw POST value as we need to tell the difference between
        # None/Undefined and an empty string.  The validators use a default
        # value with both of those cases and would need to be changed.
        # In order to avoid breaking functionality, this was done instead.
        previous = previous._id if previous else request.POST.get('previous')
        try:
            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)
                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)
                    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({})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              page=VWikiPage('page'),
              act=VOneOf('act', ('del', 'add')),
              user=VExistingUname('username'))
    @api_doc(api_section.wiki,
             uri='/api/wiki/alloweditor/{act}',
             uses_site=True,
             uri_variants=[
                 '/api/wiki/alloweditor/%s' % act for act in ('del', 'add')
             ])
    def POST_wiki_allow_editor(self, act, page, user):
        """Allow/deny `username` to edit this wiki `page`"""
        if not user:
            self.handle_error(404, 'UNKNOWN_USER')
        elif act == 'del':
            page.remove_editor(user._id36)
        elif act == 'add':
            page.add_editor(user._id36)
        else:
            self.handle_error(400, 'INVALID_ACTION')
        return json.dumps({})

    @validate(
        VModhash(),
        VAdmin(),
        pv=VWikiPageAndVersion(('page', 'revision')),
        deleted=VBoolean('deleted'),
    )
    def POST_wiki_revision_delete(self, pv, deleted):
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        if deleted and page.revision == str(revision._id):
            self.handle_error(400, 'REVISION_IS_CURRENT')
        revision.admin_deleted = deleted
        revision._commit()
        return json.dumps({'status': revision.admin_deleted})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              pv=VWikiPageAndVersion(('page', 'revision')))
    @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True)
    def POST_wiki_revision_hide(self, pv):
        """Toggle the public visibility of a wiki page revision"""
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        return json.dumps({'status': revision.toggle_hide()})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              pv=VWikiPageAndVersion(('page', 'revision')))
    @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True)
    def POST_wiki_revision_revert(self, pv):
        """Revert a wiki `page` to `revision`"""
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        content = revision.content
        reason = 'reverted back %s' % timesince(revision.date)
        if page.name == 'config/stylesheet':
            css_errors, parsed = c.site.parse_css(content)
            if css_errors:
                self.handle_error(403, 'INVALID_CSS')
            c.site.change_css(content,
                              parsed,
                              prev=None,
                              reason=reason,
                              force=True)
        else:
            try:
                page.revise(content,
                            author=c.user._id36,
                            reason=reason,
                            force=True)

                # continue storing the special pages as data attributes on the subreddit
                # object. TODO: change this to minimize subreddit get sizes.
                if page.special:
                    setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content)
                    c.site._commit()
            except ContentLengthError as e:
                self.handle_error(403,
                                  'CONTENT_LENGTH_ERROR',
                                  max_length=e.max_length)
        return json.dumps({})

    def pre(self):
        WikiController.pre(self)
        c.render_style = 'api'
        set_extension(request.environ, 'json')
Ejemplo n.º 6
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

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

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

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

            tag = 'unknown'

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

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

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

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

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

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

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

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

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

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

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

        if not resp_headers:
            resp_headers = {}

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

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

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

        return self.api_wrapper({})
Ejemplo n.º 7
0
class OAuth2AccessController(MinimalController):
    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        c.oauth2_client = self._get_client_auth()

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

    @validate(grant_type=VOneOf("grant_type",
                                ("authorization_code", "refresh_token")),
              code=nop("code"),
              refresh_token=VOAuth2RefreshToken("refresh_token"),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI))
    def POST_access_token(self, grant_type, code, refresh_token, redirect_uri):
        """
        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code
        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for
        an access token.

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

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

        Per the OAuth specification, **grant_type** must
        be ``authorization_code`` for the initial access token or
        ``refresh_token`` for renewing the access token. In either case,
        **redirect_uri** must exactly match the value that was used in the call
        to [/api/v1/authorize](#api_method_authorize) that created this grant.
        """

        resp = {}
        if not (code or refresh_token):
            c.errors.add("NO_TEXT", field=("code", "refresh_token"))
        if not c.errors:
            access_token = None

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

            if access_token:
                resp["access_token"] = access_token._id
                resp["token_type"] = access_token.token_type
                resp["expires_in"] = int(
                    access_token._ttl) if access_token._ttl else None
                resp["scope"] = access_token.scope
                if refresh_token:
                    resp["refresh_token"] = refresh_token._id
            else:
                resp["error"] = "invalid_grant"
        else:
            if (errors.INVALID_OPTION, "grant_type") in c.errors:
                resp["error"] = "unsupported_grant_type"
            elif (errors.INVALID_OPTION, "scope") in c.errors:
                resp["error"] = "invalid_scope"
            else:
                resp["error"] = "invalid_request"

        return self.api_wrapper(resp)
Ejemplo n.º 8
0
class SponsorListingController(PromoteListingController):
    titles = dict(
        PromoteListingController.titles.items() + {
            'underdelivered': N_('underdelivered promoted links'),
            'reported': N_('reported promoted links'),
            'house': N_('house promoted links'),
        }.items())
    base_path = '/sponsor/promoted'

    @property
    def title_text(self):
        return _('promos on reddit')

    @property
    def menus(self):
        if self.sort in {'underdelivered', 'reported', 'house'}:
            menus = []
        else:
            menus = super(SponsorListingController, self).menus
            menus.append(
                NavMenu([
                    QueryButton("exclude managed",
                                dest=None,
                                query_param='include_managed'),
                    QueryButton("include managed",
                                dest="yes",
                                query_param='include_managed'),
                ],
                        base_path=request.path,
                        type='lightdrop'))

        if self.sort == 'live_promos':
            srnames = promote.all_live_promo_srnames()
            buttons = [NavButton('all', '', use_params=True)]
            try:
                srnames.remove(Frontpage.name)
                frontbutton = NavButton('FRONTPAGE',
                                        Frontpage.name,
                                        use_params=True,
                                        aliases=[
                                            '/promoted/live_promos/%s' %
                                            urllib.quote(Frontpage.name)
                                        ])
                buttons.append(frontbutton)
            except KeyError:
                pass

            srnames = sorted(srnames, key=lambda name: name.lower())
            buttons.extend(
                NavButton(name, name, use_params=True) for name in srnames)
            base_path = self.base_path + '/live_promos'
            menus.append(
                NavMenu(buttons,
                        base_path=base_path,
                        title='subreddit',
                        type='lightdrop'))
        return menus

    @classmethod
    @memoize('live_by_subreddit', time=300)
    def _live_by_subreddit(cls, sr_names):
        promotuples = promote.get_live_promotions(sr_names)
        return [pt.link for pt in promotuples]

    def live_by_subreddit(cls, sr):
        return cls._live_by_subreddit([sr.name])

    @classmethod
    @memoize('house_link_names', time=60)
    def get_house_link_names(cls):
        now = promote.promo_datetime_now()
        campaign_ids = PromotionWeights.get_campaign_ids(now)
        q = PromoCampaign._query(PromoCampaign.c._id.in_(campaign_ids),
                                 PromoCampaign.c.priority_name == 'house',
                                 data=True)
        link_names = {
            Link._fullname_from_id36(to36(camp.link_id))
            for camp in q
        }
        return sorted(link_names, reverse=True)

    def keep_fn(self):
        base_keep_fn = PromoteListingController.keep_fn(self)

        def keep(item):
            if not self.include_managed and item.managed_promo:
                return False
            return base_keep_fn(item)

        return keep

    def query(self):
        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(Subreddit.get_promote_srid())
        elif self.sort == 'house':
            return self.get_house_link_names()
        elif self.sort == 'all':
            return queries.get_all_promoted_links()

    @validate(
        VSponsorAdmin(),
        srname=nop('sr'),
        include_managed=VBoolean("include_managed"),
    )
    def GET_listing(self,
                    srname=None,
                    include_managed=False,
                    sort="all",
                    **kw):
        self.sort = sort
        self.sr = None
        self.include_managed = include_managed

        if srname:
            try:
                self.sr = Subreddit._by_name(srname)
            except NotFound:
                pass
        return ListingController.GET_listing(self, **kw)
Ejemplo n.º 9
0
class OAuth2AccessController(MinimalController):
    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        c.oauth2_client = self._get_client_auth()

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

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

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

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

        Per the OAuth specification, **grant_type** must
        be ``authorization_code`` for the initial access token or
        ``refresh_token`` for renewing the access token.

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

        if grant_type == "authorization_code":
            return self._access_token_code()
        elif grant_type == "refresh_token":
            return self._access_token_refresh()
        elif grant_type == "password":
            return self._access_token_password()
        else:
            resp = {"error": "unsupported_grant_type"}
            return self.api_wrapper(resp)

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

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

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

        access_token = None
        refresh_token = None

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

        resp = self._make_token_dict(access_token, refresh_token)

        return self.api_wrapper(resp)

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

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

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

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

        access_token = OAuth2AccessToken._new(client._id, user._id36, scope)
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)
Ejemplo n.º 10
0
class PromoteController(ListingController):
    where = 'promoted'
    render_cls = PromotePage

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

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

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

    @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()
Ejemplo n.º 11
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   collection=VCollection('collection'),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'),
                   platform=VOneOf('platform', ('mobile', 'desktop', 'all'), 
                                   default='all'))
    def GET_check_inventory(self, responder, sr, collection, location, start,
                            end, platform):
        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, platform=platform,
                        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'))

    @validatedMultipartForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True,
                   rate_ip=True,
                   prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        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),
        third_party_tracking=VUrl("third_party_tracking"),
        third_party_tracking_2=VUrl("third_party_tracking_2"),
        is_managed=VBoolean("is_managed"),
        thumbnail_file=VUploadLength('file', 500*1024),
    )
    def POST_create_promo(self, form, jquery, username, title, url,
                          selftext, kind, disable_comments, sendreplies,
                          media_url, media_autoplay, media_override,
                          iframe_embed_url, media_url_type, domain_override,
                          third_party_tracking, third_party_tracking_2,
                          is_managed, thumbnail_file):
        return self._edit_promo(form, jquery, username, title, url,
                                selftext, kind, disable_comments, sendreplies,
                                media_url, media_autoplay, media_override,
                                iframe_embed_url, media_url_type, domain_override,
                                third_party_tracking, third_party_tracking_2,
                                is_managed, thumbnail_file=thumbnail_file)

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True,
                   rate_ip=True,
                   prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        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),
        third_party_tracking=VUrl("third_party_tracking"),
        third_party_tracking_2=VUrl("third_party_tracking_2"),
        is_managed=VBoolean("is_managed"),
        l=VLink('link_id36'),
    )
    def POST_edit_promo(self, form, jquery, username, title, url,
                        selftext, kind, disable_comments, sendreplies,
                        media_url, media_autoplay, media_override,
                        iframe_embed_url, media_url_type, domain_override,
                        third_party_tracking, third_party_tracking_2,
                        is_managed, l):
        return self._edit_promo(form, jquery, username, title, url,
                                selftext, kind, disable_comments, sendreplies,
                                media_url, media_autoplay, media_override,
                                iframe_embed_url, media_url_type, domain_override,
                                third_party_tracking, third_party_tracking_2,
                                is_managed, l=l)

    def _edit_promo(self, form, jquery, username, title, url,
                    selftext, kind, disable_comments, sendreplies,
                    media_url, media_autoplay, media_override,
                    iframe_embed_url, media_url_type, domain_override,
                    third_party_tracking, third_party_tracking_2,
                    is_managed, l=None, thumbnail_file=None):
        should_ratelimit = False
        is_self = kind == "self"
        is_link = not is_self
        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)
            if c.user_is_sponsor:
                l.managed_promo = is_managed
                l.domain_override = domain_override or None
                l.third_party_tracking = third_party_tracking or None
                l.third_party_tracking_2 = third_party_tracking_2 or None
            l._commit()

            # only set the thumbnail when creating a link
            if thumbnail_file:
                try:
                    force_thumbnail(l, thumbnail_file)
                    l._commit()
                except IOError:
                    pass

            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.third_party_tracking = third_party_tracking or None
            l.third_party_tracking_2 = third_party_tracking_2 or None
            l.managed_promo = is_managed

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

    @validatedForm(
        VSponsorAdmin(),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        sr=VSubmitSR('sr', promotion=True),
    )
    def POST_add_roadblock(self, form, jquery, start, end, sr):
        if (form.has_errors('startdate', errors.BAD_DATE) or
                form.has_errors('enddate', errors.BAD_DATE)):
            return

        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', errors.BAD_DATE_RANGE)
            return

        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return

        PromotedLinkRoadblock.add(sr, start, end)
        jquery.refresh()

    @validatedForm(
        VSponsorAdmin(),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        sr=VSubmitSR('sr', promotion=True),
    )
    def POST_rm_roadblock(self, form, jquery, start, end, sr):
        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', errors.BAD_DATE_RANGE)
            return

        if start and end and sr:
            PromotedLinkRoadblock.remove(sr, start, end)
            jquery.refresh()

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        link=VLink('link_id36'),
        bid=VFloat('bid', coerce=False),
        target=VPromoTarget(),
        campaign_id36=nop("campaign_id36"),
        priority=VPriority("priority"),
        location=VLocation(),
        platform=VOneOf("platform", ("mobile", "desktop", "all"), default="desktop"),
        mobile_os=VList("mobile_os", choices=["iOS", "Android"]),
    )
    def POST_edit_campaign(self, form, jquery, link, campaign_id36,
                           start, end, bid, target, priority, location,
                           platform, mobile_os):
        if not link:
            return

        if platform in ('mobile', 'all') and not mobile_os:
            c.errors.add(errors.BAD_PROMO_MOBILE_OS, field='mobile_os')
            form.set_error(errors.BAD_PROMO_MOBILE_OS, 'mobile_os')
            return

        if platform == 'mobile' and priority.cpm:
            c.errors.add(errors.BAD_PROMO_MOBILE_PRIORITY, field='priority')
            form.set_error(errors.BAD_PROMO_MOBILE_PRIORITY, 'priority')
            return

        if not (c.user_is_sponsor or platform == 'desktop'):
            return abort(403, 'forbidden')

        if platform == 'desktop':
            mobile_os = None

        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

        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) or
                form.has_errors('enddate', errors.BAD_DATE)):
            return

        min_start, max_start, max_end = promote.get_date_limits(
            link, c.user_is_sponsor)

        if campaign_id36:
            promo_campaign = PromoCampaign._byID36(campaign_id36)
            if (promote.is_promoted(link) and
                    promo_campaign.start_date.date() <= min_start and
                    start != promo_campaign.start_date and
                    promo_campaign.is_paid):
                c.errors.add(errors.START_DATE_CANNOT_CHANGE, field='startdate')
                form.has_errors('startdate', errors.START_DATE_CANNOT_CHANGE)
                return
        elif start.date() < min_start:
            c.errors.add(errors.DATE_TOO_EARLY,
                         msg_params={'day': min_start.strftime("%m/%d/%Y")},
                         field='startdate')
            form.has_errors('startdate', errors.DATE_TOO_EARLY)
            return

        if start.date() > 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

        if end.date() > max_end:
            c.errors.add(errors.DATE_TOO_LATE,
                         msg_params={'day': max_end.strftime("%m/%d/%Y")},
                         field='enddate')
            form.has_errors('enddate', errors.DATE_TOO_LATE)
            return

        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', 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, 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

        dates = (start, end)
        if campaign:
            promote.edit_campaign(link, campaign, dates, bid, cpm, target,
                                  priority, location, platform, mobile_os)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, target,
                                            priority, location, platform, mobile_os)
        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 the campaign dates are still valid (user may have created
        # the campaign a few days ago)
        min_start, max_start, max_end = promote.get_date_limits(
            link, c.user_is_sponsor)

        if campaign.start_date.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

        if campaign.start_date.date() < min_start:
            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

        new_payment = not pay_id

        address_modified = new_payment 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:
                hooks.get_hook("promote.campaign_paid").call(link=link, campaign=campaign)
                if not address and g.authorizenetapi:
                    profiles = get_account_info(c.user).paymentProfiles
                    profile = {p.customerPaymentProfileId: p for p in profiles}[pay_id]

                    address = profile.billTo

                promote.successful_payment(link, campaign, request.ip, address)

                jquery.payment_redirect(promote.promo_edit_url(link), new_payment, campaign.bid)
                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 not link or (promote.is_promoted(link) and not c.user_is_sponsor):
            # only let sponsors edit thumbnails of live promos
            return abort(403, 'forbidden')

        force_thumbnail(link, file, file_type=".%s" % img_type)
        link._commit()
        return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors,
                             form_id="image-upload").render()

    @validate(
        VSponsor("link_name"),
        VModhash(),
        link=VByName('link_name'),
        file=VUploadLength('file', 500*1024),
        img_type=VImageType('img_type'),
    )
    def POST_link_mobile_ad_image(self, link=None, file=None, img_type='jpg'):
        if not (link and c.user_is_sponsor and file):
            # only sponsors can set the mobile img
            return abort(403, 'forbidden')

        force_mobile_ad_image(link, file, file_type=".%s" % img_type)
        link._commit()
        return UploadedImage(_('saved'), link.mobile_ad_url, "", errors=errors,
                             form_id="mobile-ad-image-upload").render()
Ejemplo n.º 12
0
class OAuth2AccessController(MinimalController):
    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        c.oauth2_client = self._get_client_auth()

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

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

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

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

        Per the OAuth specification, **grant_type** must
        be ``authorization_code`` for the initial access token or
        ``refresh_token`` for renewing the access token.

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

        if grant_type == "authorization_code":
            return self._access_token_code()
        elif grant_type == "refresh_token":
            return self._access_token_refresh()
        elif grant_type == "password":
            return self._access_token_password()
        else:
            resp = {"error": "unsupported_grant_type"}
            return self.api_wrapper(resp)

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

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

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

        access_token = None
        refresh_token = None

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

        resp = self._make_token_dict(access_token, refresh_token)

        return self.api_wrapper(resp)

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

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

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

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

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

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

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

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

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

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

        if not token_id:
            return

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

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

        if constant_time_compare(token.client_id, c.oauth2_client._id):
            token.revoke()
        else:
            # RFC 7009 is not clear on how to handle this case.
            # Given that a malicious client could do much worse things
            # with a valid token then revoke it, returning an error
            # here is best as it may help certain clients debug issues
            response.status = 400
            return self.api_wrapper({"error": "unauthorized_client"})
Ejemplo n.º 13
0
class AdzerkApiController(api.ApiController):
    @csrf_exempt
    @allow_oauth2_access
    @validate(
        srnames=VPrintable("srnames", max_length=2100),
        is_mobile_web=VBoolean('is_mobile_web'),
        platform=VOneOf("platform", [
            "desktop",
            "mobile_web",
            "mobile_native",
        ],
                        default=None),
        loid=nop('loid', None),
        is_refresh=VBoolean("is_refresh", default=False),
    )
    def POST_request_promo(self, srnames, is_mobile_web, platform, loid,
                           is_refresh):
        self.OPTIONS_request_promo()

        if not srnames:
            return

        # backwards compat
        if platform is None:
            platform = "mobile_web" if is_mobile_web else "desktop"

        srnames = srnames.split('+')

        # request multiple ads in case some are hidden by the builder due
        # to the user's hides/preferences
        response = adzerk_request(srnames,
                                  self.get_uid(loid),
                                  platform=platform)

        if not response:
            g.stats.simple_event('adzerk.request.no_promo')
            return

        # for adservers, adzerk returns markup so we pass it to the client
        if isinstance(response, AdserverResponse):
            g.stats.simple_event('adzerk.request.adserver')
            return responsive(response.body)

        res_by_campaign = {r.campaign: r for r in response}
        adserver_click_urls = {r.campaign: r.click_url for r in response}
        tuples = [promote.PromoTuple(r.link, 1., r.campaign) for r in response]
        builder = CampaignBuilder(tuples,
                                  wrap=default_thing_wrapper(),
                                  keep_fn=promote.promo_keep_fn,
                                  num=1,
                                  skip=True)
        listing = LinkListing(builder, nextprev=False).listing()
        promote.add_trackers(listing.things,
                             c.site,
                             adserver_click_urls=adserver_click_urls)
        promote.update_served(listing.things)
        if listing.things:
            g.stats.simple_event('adzerk.request.valid_promo')
            if is_refresh:
                g.stats.simple_event('adzerk.request.auto_refresh')

            w = listing.things[0]
            r = res_by_campaign[w.campaign]

            up = UrlParser(r.imp_pixel)
            up.hostname = "pixel.redditmedia.com"
            w.adserver_imp_pixel = up.unparse()
            w.adserver_upvote_pixel = r.upvote_pixel
            w.adserver_downvote_pixel = r.downvote_pixel
            w.adserver_click_url = r.click_url
            w.num = ""
            return responsive(w.render(), space_compress=True)
        else:
            g.stats.simple_event('adzerk.request.skip_promo')

    def get_uid(self, loid):
        if c.user_is_loggedin:
            return c.user._id36
        elif loid:
            return loid
        else:
            return None
Ejemplo n.º 14
0
class StripeController(GoldPaymentController):
    name = 'stripe'
    webhook_secret = g.secrets['stripe_webhook']
    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)
        stripe_secret = g.secrets['stripe_secret_key']
        event = stripe.Event.construct_from(event_dict, stripe_secret)
        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"))
Ejemplo n.º 15
0
class ButtonApiController(ApiController):
    @validate(
        VUser(),
        VModhash(),
        seconds_remaining=VInt('seconds', min=0, max=60),
        previous_seconds=VInt('prev_seconds'),
        tick_time=nop('tick_time'),
        tick_mac=nop('tick_mac'),
    )
    def POST_press_button(self, seconds_remaining, previous_seconds, tick_time, tick_mac):
        if not g.live_config['thebutton_is_active']:
            return

        if c.user._date > ACCOUNT_CREATION_CUTOFF:
            return

        user_has_pressed = ButtonPressByUser.has_pressed(c.user)

        if user_has_pressed and not c.user.employee:
            return

        if has_timer_expired():
            # time has expired: no longer possible to press the button
            return

        has_started = has_timer_started()

        if not has_started:
            # the timer can only be started through reddit-shell
            return

        cheater = False
        if (seconds_remaining is None or
                previous_seconds is None or
                tick_time is None or
                tick_mac is None):
            # incomplete info from client, just let them press it anyways
            seconds_remaining = max(0, int(get_seconds_left()))
        elif not check_tick_mac(previous_seconds, tick_time, tick_mac):
            # can't trust the values sent by the client
            seconds_remaining = max(0, int(get_seconds_left()))
            cheater = True
        else:
            # client sent a valid mac so we can trust:
            # previous_seconds - the timer value at the last tick
            # tick_time - the datetime at the last tick

            # check to make sure tick_time wasn't too long ago
            then = str_to_datetime(tick_time)
            now = datetime.now(g.tz)
            if then and (now - then).total_seconds() > 60:
                # client sent an old (but potentially valid) mac, etc.
                seconds_remaining = max(0, int(get_seconds_left()))
                cheater = True

            # GOTCHA: the client actually sends the same value for
            # previous_seconds and seconds_remaining so make sure those match.
            # If the client sent down its own ticking down timer as
            # seconds_remaining we would want to compare to previous_seconds to
            # make sure they weren't too far apart
            if previous_seconds != seconds_remaining:
                seconds_remaining = max(0, int(get_seconds_left()))
                cheater = True

        press_button(c.user)
        g.stats.simple_event("thebutton.press")
        if cheater:
            g.stats.simple_event("thebutton.cheater")

        # don't flair on first press (the starter)
        if not has_started:
            return

        if user_has_pressed:
            # don't flair on multiple employee presses
            return

        if cheater:
            flair_css = "cheater"
        elif seconds_remaining > 51:
            flair_css = "press-6"
        elif seconds_remaining > 41:
            flair_css = "press-5"
        elif seconds_remaining > 31:
            flair_css = "press-4"
        elif seconds_remaining > 21:
            flair_css = "press-3"
        elif seconds_remaining > 11:
            flair_css = "press-2"
        else:
            flair_css = "press-1"

        flair_text = "%ss" % seconds_remaining

        setattr(c.user, 'flair_%s_text' % g.live_config["thebutton_srid"], flair_text)
        setattr(c.user, 'flair_%s_css_class' % g.live_config["thebutton_srid"], flair_css)
        c.user._commit()
Ejemplo n.º 16
0
class SponsorController(PromoteController):
    @validate(VSponsorAdmin())
    def GET_roadblock(self):
        return PromotePage(title=_("manage roadblocks"),
                           content=Roadblocks()).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)
        if not start or not end:
            start = promote.promo_datetime_now(offset=1).date()
            end = promote.promo_datetime_now(offset=8).date()
            c.errors.remove((errors.BAD_DATE, 'startdate'))
            c.errors.remove((errors.BAD_DATE, 'enddate'))
        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:
            campaign_ids = PromotionWeights.get_campaign_ids(
                start, end, author_id=owner._id)
            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(title=_("sponsored link report"),
                               content=content).render()

    @validate(
        VSponsorAdmin(),
        start=VDate('startdate', reference_date=promote.promo_datetime_now),
        end=VDate('enddate', reference_date=promote.promo_datetime_now),
        sr_name=nop('sr_name'),
        collection_name=nop('collection_name'),
    )
    def GET_promote_inventory(self, start, end, sr_name, collection_name):
        if not start or not end:
            start = promote.promo_datetime_now(offset=1).date()
            end = promote.promo_datetime_now(offset=8).date()
            c.errors.remove((errors.BAD_DATE, 'startdate'))
            c.errors.remove((errors.BAD_DATE, 'enddate'))

        target = Target(Frontpage.name)
        if sr_name:
            try:
                sr = Subreddit._by_name(sr_name)
                target = Target(sr.name)
            except NotFound:
                c.errors.add(errors.SUBREDDIT_NOEXIST, field='sr_name')
        elif collection_name:
            collection = Collection.by_name(collection_name)
            if not collection:
                c.errors.add(errors.COLLECTION_NOEXIST,
                             field='collection_name')
            else:
                target = Target(collection)

        content = PromoteInventory(start, end, target)
        return PromotePage(title=_("sponsored link inventory"),
                           content=content).render()
Ejemplo n.º 17
0
class StripeController(GoldPaymentController):
    name = 'stripe'
    webhook_secret = g.STRIPE_WEBHOOK_SECRET
    event_type_mappings = {
        'charge.succeeded': 'succeeded',
        'charge.failed': 'failed',
        'charge.refunded': 'refunded',
        'customer.created': 'noop',
        'customer.card.created': 'noop',
        'transfer.created': 'noop',
        'transfer.paid': 'noop',
        'balance.available': '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
        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, None, None, None

        charge = event.data.object
        description = charge.description
        try:
            passthrough, buyer_name = description.split('-', 1)
        except ValueError:
            g.log.error('stripe_error on charge: %s', charge)
            raise
        transaction_id = 'S%s' % charge.id
        pennies = charge.amount
        months, days = months_and_days_from_pennies(pennies)
        return status, passthrough, transaction_id, pennies, months

    @validatedForm(VUser(),
                   token=nop('stripeToken'),
                   passthrough=VPrintable("passthrough", max_length=50),
                   pennies=VInt('pennies'),
                   months=VInt("months"))
    def POST_goldcharge(self, form, jquery, token, passthrough, pennies,
                        months):
        """
        Submit charge to stripe.

        Called by GoldPayment form. This submits the charge to stripe, and gold
        will be applied once we receive a webhook from stripe.

        """

        try:
            payment_blob = validate_blob(passthrough)
        except GoldException as e:
            # This should never happen. All fields in the payment_blob
            # are validated on creation
            form.set_html('.status',
                          _('something bad happened, try again later'))
            g.log.debug('POST_goldcharge: %s' % e.message)
            return

        penny_months, days = months_and_days_from_pennies(pennies)
        if not months or months != penny_months:
            form.set_html('.status', _('stop trying to trick the form'))
            return

        stripe.api_key = g.STRIPE_SECRET_KEY

        try:
            customer = stripe.Customer.create(card=token)

            if (customer['active_card']['address_line1_check'] == 'fail'
                    or customer['active_card']['address_zip_check'] == 'fail'):
                form.set_html('.status',
                              _('error: address verification failed'))
                form.find('.stripe-submit').removeAttr('disabled').end()
                return

            if customer['active_card']['cvc_check'] == 'fail':
                form.set_html('.status', _('error: cvc check failed'))
                form.find('.stripe-submit').removeAttr('disabled').end()
                return

            charge = stripe.Charge.create(amount=pennies,
                                          currency="usd",
                                          customer=customer['id'],
                                          description='%s-%s' %
                                          (passthrough, c.user.name))
        except stripe.CardError as e:
            form.set_html('.status', 'error: %s' % e.message)
            form.find('.stripe-submit').removeAttr('disabled').end()
        except stripe.InvalidRequestError as e:
            form.set_html('.status', _('invalid request'))
        except stripe.APIConnectionError as e:
            form.set_html('.status', _('api error'))
        except stripe.AuthenticationError as e:
            form.set_html('.status', _('connection error'))
        except stripe.StripeError as e:
            form.set_html('.status', _('error'))
            g.log.error('stripe error: %s' % e)
        else:
            form.set_html('.status', _('payment submitted'))

            # webhook usually sends near instantly, send a message in case
            subject = _('reddit gold payment')
            msg = _('Your payment is being processed and reddit gold will be '
                    'delivered shortly.')
            send_system_message(c.user, subject, msg)
Ejemplo n.º 18
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   collection=VCollection('collection'),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, collection, location, start,
                            end):
        if collection:
            target = Target(collection)
            sr = None
        else:
            sr = sr or Frontpage
            target = Target(sr.name)

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

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

        return {'inventory': available}

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

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

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

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

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

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

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

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

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        l=VLink('link_id36'),
        title=VTitle('title'),
        url=VUrl('url', allow_self=False),
        selftext=VMarkdownLength('text', max_length=40000),
        kind=VOneOf('kind', ['link', 'self']),
        disable_comments=VBoolean("disable_comments"),
        sendreplies=VBoolean("sendreplies"),
        media_url=VUrl("media_url",
                       allow_self=False,
                       valid_schemes=('http', 'https')),
        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, 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:
            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()

        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
            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 (not media_url
                        and form.has_errors("media_url", errors.BAD_URL)):
                    return

                media_url = media_url or None
                media_changed = (media_url != l.media_url
                                 or media_autoplay != l.media_autoplay)

                if media_changed:
                    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.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.media_autoplay = False

                l.media_override = media_override
                l.domain_override = domain_override or None
                l.managed_promo = is_managed
            l._commit()

        form.redirect(promote.promo_edit_url(l))

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

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

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

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

        start, end = dates or (None, None)

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

        cpm = PromotionPrices.get_price(target, location)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        promote.delete_campaign(l, campaign)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            if any(errors.values()):
                return UploadedImage("",
                                     "",
                                     "upload",
                                     errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'),
                                     thumbnail_url(link),
                                     "",
                                     errors=errors,
                                     form_id="image-upload").render()
Ejemplo n.º 19
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, location, start, end):
        sr = sr or Frontpage
        if not location or not location.country:
            available = inventory.get_available_pageviews(sr,
                                                          start,
                                                          end,
                                                          datestr=True)
        else:
            available = inventory.get_available_pageviews_geotargeted(
                sr, location, start, end, datestr=True)
        return {'inventory': available}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        form.redirect(promote.promo_edit_url(l))

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

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

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

        start, end = dates or (None, None)

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

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

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

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

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

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

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

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

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

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

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

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

        elif targeting == 'none':
            sr = None

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

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

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

        promote.delete_campaign(l, campaign)

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

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

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

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

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

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

    def pre(self):
        if g.disallow_db_writes:
            abort(403)

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

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

    def OPTIONS_access_token(self):
        """Send CORS headers for access token requests

        * Allow all origins
        * Only POST requests allowed to /api/v1/access_token
        * No ambient credentials
        * Authorization header required to identify the client
        * Expose common reddit headers

        """
        if "Origin" in request.headers:
            response.headers["Access-Control-Allow-Origin"] = "*"
            response.headers["Access-Control-Allow-Methods"] = \
                "POST"
            response.headers["Access-Control-Allow-Headers"] = \
                    "Authorization, "
            response.headers["Access-Control-Allow-Credentials"] = "false"
            response.headers['Access-Control-Expose-Headers'] = \
                self.COMMON_REDDIT_HEADERS

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

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

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

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

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

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

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

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

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

    @classmethod
    def _make_new_token_response(cls, access_token, refresh_token=None):
        if not access_token:
            return {"error": "invalid_grant"}
        access_token_ttl = getattr(access_token, 'token_ttl', None)

        if access_token_ttl:
            g.stats.simple_event(
                'oauth2.access_token.expires_in.valid_token_ttl')
            expires_in = int(access_token_ttl)
        else:
            g.stats.simple_event(
                'oauth2.access_token.expires_in.default_token_ttl')
            expires_in = g.default_access_token_ttl
        resp = {
            "access_token": access_token._id,
            "token_type": access_token.token_type,
            "expires_in": expires_in,
            "scope": access_token.scope,
        }
        if refresh_token:
            resp["refresh_token"] = refresh_token._id

        if access_token.device_id:
            resp['device_id'] = access_token.device_id

        return resp

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

        access_token = None
        refresh_token = None

        client = c.oauth2_client
        auth_token = OAuth2AuthorizationCode.use_token(code, client._id,
                                                       redirect_uri)

        if auth_token:
            if auth_token.refreshable:
                refresh_token = OAuth2RefreshToken._new(
                    client_id=auth_token.client_id,
                    user_id=auth_token.user_id,
                    scope=auth_token.scope,
                )
                g.stats.simple_event(
                    'oauth2.access_token_code.refresh_token_create')

            device_id = get_device_id(client)
            access_token = OAuth2AccessToken._new(
                client_id=auth_token.client_id,
                user_id=auth_token.user_id,
                scope=auth_token.scope,
                refresh_token=refresh_token._id if refresh_token else "",
                device_id=device_id,
            )
            g.stats.simple_event(
                'oauth2.access_token_code.access_token_create')

        resp = self._make_new_token_response(access_token, refresh_token)

        return self.api_wrapper(resp)

    @validate(refresh_token=VOAuth2RefreshToken("refresh_token"))
    def _access_token_refresh(self, refresh_token):
        access_token = None
        if refresh_token:
            if refresh_token.client_id == c.oauth2_client._id:
                access_token = OAuth2AccessToken._new(
                    refresh_token.client_id,
                    refresh_token.user_id,
                    refresh_token.scope,
                    refresh_token=refresh_token._id,
                    device_id=refresh_token.device_id)
                g.stats.simple_event(
                    'oauth2.access_token_refresh.access_token_create')
            else:
                g.stats.simple_event(
                    'oauth2.errors.OAUTH2_INVALID_REFRESH_TOKEN')
                c.errors.add(errors.OAUTH2_INVALID_REFRESH_TOKEN)
        else:
            g.stats.simple_event('oauth2.errors.NO_TEXT')
            c.errors.add("NO_TEXT", field="refresh_token")

        if c.errors:
            resp = self._check_for_errors()
            response.status = 400
        else:
            g.stats.simple_event('oauth2.access_token_refresh.success')
            resp = self._make_new_token_response(access_token)
        return self.api_wrapper(resp)

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

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

        device_id = get_device_id(client)
        access_token = OAuth2AccessToken._new(
            client_id=client._id,
            user_id=user._id36,
            scope=scope,
            device_id=device_id,
        )
        g.stats.simple_event(
            'oauth2.access_token_password.access_token_create')
        resp = self._make_new_token_response(access_token)
        return self.api_wrapper(resp)

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

        device_id = get_device_id(client)
        access_token = OAuth2AccessToken._new(
            client_id=client._id,
            user_id="",
            scope=scope,
            device_id=device_id,
        )
        g.stats.simple_event(
            'oauth2.access_token_client_credentials.access_token_create')
        resp = self._make_new_token_response(access_token)
        return self.api_wrapper(resp)

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

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

        access_token = OAuth2AccessToken._new(
            client_id=client._id,
            user_id="",
            scope=scope,
            device_id=device_id,
        )
        g.stats.simple_event(
            'oauth2.access_token_extension_client_credentials.'
            'access_token_create')
        resp = self._make_new_token_response(access_token)
        return self.api_wrapper(resp)

    def OPTIONS_revoke_token(self):
        """Send CORS headers for token revocation requests

        * Allow all origins
        * Only POST requests allowed to /api/v1/revoke_token
        * No ambient credentials
        * Authorization header required to identify the client
        * Expose common reddit headers

        """
        if "Origin" in request.headers:
            response.headers["Access-Control-Allow-Origin"] = "*"
            response.headers["Access-Control-Allow-Methods"] = \
                "POST"
            response.headers["Access-Control-Allow-Headers"] = \
                    "Authorization, "
            response.headers["Access-Control-Allow-Credentials"] = "false"
            response.headers['Access-Control-Expose-Headers'] = \
                self.COMMON_REDDIT_HEADERS

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

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

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

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

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

        if not token_id:
            return

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

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

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

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

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

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

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

    GET_index = GET_listing

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

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

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

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

        return page.render()


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

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

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

        return page.render()


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

        form.redirect(promote.promo_edit_url(l))

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

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

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

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

        start, end = dates or (None, None)

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


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

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

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

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

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

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

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

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

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

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


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

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


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


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

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

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

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

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

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