Exemplo n.º 1
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

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

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

        VRatelimit.ratelimit(rate_user=False, rate_ip=True,
                             prefix="rate_weblog_", seconds=10)
Exemplo n.º 2
0
Arquivo: web.py Projeto: znanl/reddit
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

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

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

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

            tag = 'unknown'

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

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

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

        VRatelimit.ratelimit(rate_user=False,
                             rate_ip=True,
                             prefix="rate_weblog_",
                             seconds=10)
Exemplo n.º 3
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

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

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

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

            tag = 'unknown'

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

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

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

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

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

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

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

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

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

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

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

        if not resp_headers:
            resp_headers = {}

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

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

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

        return self.api_wrapper({})
Exemplo n.º 4
0
class APIv1UserController(OAuth2OnlyController):
    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(api_section.account)
    def GET_me(self):
        """Returns the identity of the user currently authenticated via OAuth."""
        resp = IdentityJsonTemplate().data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("identity")
    @validate(
        VUser(),
        fields=VList(
            "fields",
            choices=PREFS_JSON_SPEC.spec.keys(),
            error=errors.errors.NON_PREFERENCE,
        ),
    )
    @api_doc(api_section.account, uri='/api/v1/me/prefs')
    def GET_prefs(self, fields):
        """Return the preference settings of the logged in user"""
        resp = PrefsJsonTemplate(fields).data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName('username'), )
    @api_doc(
        section=api_section.users,
        uri='/api/v1/user/{username}/trophies',
    )
    def GET_usertrophies(self, user):
        """Return a list of trophies for the a given user."""
        return self.api_wrapper(get_usertrophies(user))

    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/trophies',
    )
    def GET_trophies(self):
        """Return a list of trophies for the current user."""
        return self.api_wrapper(get_usertrophies(c.oauth_user))

    @require_oauth2_scope("mysubreddits")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/karma',
    )
    def GET_karma(self):
        """Return a breakdown of subreddit karma."""
        karmas = c.oauth_user.all_karmas(include_old=False)
        resp = KarmaListJsonTemplate().render(karmas)
        return self.api_wrapper(resp.finalize())

    PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True)

    @require_oauth2_scope("account")
    @validate(
        VUser(),
        validated_prefs=PREFS_JSON_VALIDATOR,
    )
    @api_doc(api_section.account,
             json_model=PREFS_JSON_VALIDATOR,
             uri='/api/v1/me/prefs')
    def PATCH_prefs(self, validated_prefs):
        user_prefs = c.user.preferences()
        for short_name, new_value in validated_prefs.iteritems():
            pref_name = "pref_" + short_name
            user_prefs[pref_name] = new_value
        vprefs.filter_prefs(user_prefs, c.user)
        vprefs.set_prefs(c.user, user_prefs)
        c.user._commit()
        return self.api_wrapper(PrefsJsonTemplate().data(c.user))

    FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({
        "name":
        VAccountByName("name"),
        "note":
        VLength("note", 300),
    })
    FRIEND_JSON_VALIDATOR = VValidatedJSON("json",
                                           spec=FRIEND_JSON_SPEC,
                                           body=True)

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend=VAccountByName('username'),
        notes_json=FRIEND_JSON_VALIDATOR,
    )
    @api_doc(api_section.users,
             json_model=FRIEND_JSON_VALIDATOR,
             uri='/api/v1/me/friends/{username}')
    def PUT_friends(self, friend, notes_json):
        """Create or update a "friend" relationship.

        This operation is idempotent. It can be used to add a new
        friend, or update an existing friend (e.g., add/change the
        note on that friend)

        """
        err = None
        if 'name' in notes_json and notes_json['name'] != friend:
            # The 'name' in the JSON is optional, but if present, must
            # match the username from the URL
            err = errors.RedditError('BAD_USERNAME', fields='name')
        if 'note' in notes_json and not c.user.gold:
            err = errors.RedditError('GOLD_REQUIRED', fields='note')
        if err:
            self.on_validation_error(err)

        # See if the target is already an existing friend.
        # If not, create the friend relationship.
        friend_rel = Account.get_friend(c.user, friend)
        rel_exists = bool(friend_rel)
        if not friend_rel:
            friend_rel = c.user.add_friend(friend)
            response.status = 201

        if 'note' in notes_json:
            note = notes_json['note'] or ''
            if not rel_exists:
                # If this is a newly created friend relationship,
                # the cache needs to be updated before a note can
                # be applied
                c.user.friend_rels_cache(_update=True)
            c.user.add_friend_note(friend, note)
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('mysubreddits')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def GET_friends(self, friend_rel):
        """Get information about a specific 'friend', such as notes."""
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def DELETE_friends(self, friend_rel):
        """Stop being friends with a user."""
        c.user.remove_friend(friend_rel._thing2)
        if c.user.gold:
            c.user.friend_rels_cache(_update=True)
        response.status = 204
Exemplo n.º 5
0
class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    def pre(self):
        set_extension(request.environ, "json")
        RedditController.pre(self)

    @require_oauth2_scope("read")
    @validate(VUser())
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self):
        """Fetch a list of multis belonging to the current user."""
        multis = LabeledMulti.by_owner(c.user)
        wrapped = wrap_things(*multis)
        resp = [w.render() for w in wrapped]
        return self.api_wrapper(resp)

    def _format_multi(self, multi):
        resp = wrap_things(multi)[0].render()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(multi=VMultiByPath("multipath", require_view=True))
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi)

    def _check_new_multi_path(self, path_info):
        if path_info['username'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        multi.visibility = data['visibility']

        multi.clear_srs()
        try:
            self._add_multi_srs(multi, data['subreddits'])
        except:
            multi._revert()
            raise

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info):
        self._check_new_multi_path(to_path_info)

        to_owner = Account._by_name(to_path_info['username'])

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        to_multi = self._copy_multi(from_multi, to_path_info)
        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        to_multi._commit()
        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info):
        """Rename a multi."""

        to_multi = self._copy_multi(from_multi, to_path_info)
        from_multi.delete()
        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
Exemplo n.º 6
0
class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    def pre(self):
        set_extension(request.environ, "json")
        RedditController.pre(self)

    def _format_multi_list(self, multis, viewer, expand_srs):
        templ = LabeledMultiJsonTemplate(expand_srs)
        resp = [
            templ.render(multi).finalize() for multi in multis
            if multi.can_view(viewer)
        ]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName("username"),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(api_section.multis, uri="/api/multi/user/{username}")
    def GET_list_multis(self, user, expand_srs):
        """Fetch a list of public multis belonging to `username`"""
        multis = LabeledMulti.by_owner(user)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(
        sr=VSRByName('srname'),
        expand_srs=VBoolean("expand_srs"),
    )
    def GET_list_sr_multis(self, sr, expand_srs):
        """Fetch a list of public multis belonging to subreddit `srname`"""
        multis = LabeledMulti.by_owner(sr)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(VUser(), expand_srs=VBoolean("expand_srs"))
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self, expand_srs):
        """Fetch a list of multis belonging to the current user."""
        multis = LabeledMulti.by_owner(c.user)
        return self._format_multi_list(multis, c.user, expand_srs)

    def _format_multi(self, multi, expand_sr_info=False):
        multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi)
        return self.api_wrapper(multi_info.finalize())

    @require_oauth2_scope("read")
    @validate(
        multi=VMultiByPath("multipath", require_view=True),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi, expand_srs):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi, expand_srs)

    def _check_new_multi_path(self, path_info):
        if path_info['prefix'] == 'r':
            return self._check_sr_multi_path(path_info)

        return self._check_user_multi_path(path_info)

    def _check_user_multi_path(self, path_info):
        if path_info['owner'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')
        return c.user

    def _check_sr_multi_path(self, path_info):
        try:
            sr = Subreddit._by_name(path_info['owner'])
        except NotFound:
            raise RedditError('SUBREDDIT_NOEXIST', code=404)

        if (not sr.is_moderator_with_perms(c.user, 'config')
                and not c.user_is_admin):
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

        return sr

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        srs = data.pop('subreddits', None)
        if srs is not None:
            multi.clear_srs()
            try:
                self._add_multi_srs(multi, srs)
            except:
                multi._revert()
                raise

        if 'icon_name' in data:
            try:
                multi.set_icon_by_name(data.pop('icon_name'))
            except:
                multi._revert()
                raise

        for key, val in data.iteritems():
            if key in WRITABLE_MULTI_FIELDS:
                setattr(multi, key, val)

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath", required=False),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        if not path_info and "path" in data:
            path_info = VMultiPath("").run(data["path"])
        elif 'display_name' in data:
            # if path not provided, create multi for user
            path = LabeledMulti.slugify(c.user, data['display_name'])
            path_info = VMultiPath("").run(path)

        if not path_info:
            raise RedditError('BAD_MULTI_PATH', code=400)

        owner = self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        owner = self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info, rename=False):
        """Copy a multi to a user account."""

        to_owner = self._check_new_multi_path(to_path_info)

        # rename requires same owner
        if rename and from_multi.owner != to_owner:
            raise RedditError('MULTI_CANNOT_EDIT', code=400)

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info, display_name):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        if not to_path_info:
            if display_name:
                # if path not provided, copy multi to same owner
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info)

        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        if display_name:
            to_multi.display_name = display_name
        to_multi._commit()

        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info, display_name):
        """Rename a multi."""
        if not to_path_info:
            if display_name:
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info, rename=True)

        if display_name:
            to_multi.display_name = display_name
            to_multi._commit()
        from_multi.delete()

        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
Exemplo n.º 7
0
class WebLogController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

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

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

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

            tag = 'unknown'

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

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

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

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

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

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

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

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

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

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

        abort(204)
Exemplo n.º 8
0
class APIv1Controller(OAuth2ResourceController):
    def pre(self):
        OAuth2ResourceController.pre(self)
        self.authenticate_with_token()
        self.run_sitewide_ratelimits()

    def try_pagecache(self):
        pass

    @staticmethod
    def on_validation_error(error):
        abort_with_error(error, error.code or 400)

    @require_oauth2_scope("identity")
    @api_doc(api_section.account)
    def GET_me(self):
        """Returns the identity of the user currently authenticated via OAuth."""
        resp = IdentityJsonTemplate().data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("identity")
    @validate(
        fields=VList(
            "fields",
            choices=PREFS_JSON_SPEC.spec.keys(),
            error=errors.errors.NON_PREFERENCE,
        ), )
    @api_doc(api_section.account, uri='/api/v1/me/prefs')
    def GET_prefs(self, fields):
        """Return the preference settings of the logged in user"""
        resp = PrefsJsonTemplate(fields).data(c.oauth_user)
        return self.api_wrapper(resp)

    def _get_usertrophies(self, user):
        trophies = Trophy.by_account(user)

        def visible_trophy(trophy):
            return trophy._thing2.awardtype != 'invisible'

        trophies = filter(visible_trophy, trophies)
        resp = TrophyListJsonTemplate().render(trophies)
        return self.api_wrapper(resp.finalize())

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName('username'), )
    @api_doc(
        section=api_section.users,
        uri='/api/v1/user/{username}/trophies',
    )
    def GET_usertrophies(self, user):
        """Return a list of trophies for the a given user."""
        return self._get_usertrophies(user)

    @require_oauth2_scope("identity")
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/trophies',
    )
    def GET_trophies(self):
        """Return a list of trophies for the current user."""
        return self._get_usertrophies(c.oauth_user)

    @require_oauth2_scope("mysubreddits")
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/karma',
    )
    def GET_karma(self):
        """Return a breakdown of subreddit karma."""
        karmas = c.oauth_user.all_karmas(include_old=False)
        resp = KarmaListJsonTemplate().render(karmas)
        return self.api_wrapper(resp.finalize())

    PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True)

    @require_oauth2_scope("account")
    @validate(validated_prefs=PREFS_JSON_VALIDATOR)
    @api_doc(api_section.account,
             json_model=PREFS_JSON_VALIDATOR,
             uri='/api/v1/me/prefs')
    def PATCH_prefs(self, validated_prefs):
        user_prefs = c.user.preferences()
        for short_name, new_value in validated_prefs.iteritems():
            pref_name = "pref_" + short_name
            if pref_name == "pref_content_langs":
                new_value = vprefs.format_content_lang_pref(new_value)
            user_prefs[pref_name] = new_value
        vprefs.filter_prefs(user_prefs, c.user)
        vprefs.set_prefs(c.user, user_prefs)
        c.user._commit()
        return self.api_wrapper(PrefsJsonTemplate().data(c.user))