def POST_wiki_settings(self, page, permlevel, listed): """Update the permissions and visibility of wiki `page`""" oldpermlevel = page.permlevel if oldpermlevel != permlevel: VNotInTimeout().run(action_name="wikipermlevel", details_text="edit", target=page) if page.listed != listed: VNotInTimeout().run(action_name="wikipagelisted", details_text="edit", target=page) try: page.change_permlevel(permlevel) except ValueError: self.handle_error(403, 'INVALID_PERMLEVEL') if page.listed != listed: page.listed = listed page._commit() verb = 'Relisted' if listed else 'Delisted' description = '%s page %s' % (verb, page.name) ModAction.create(c.site, c.user, 'wikipagelisted', description=description) if oldpermlevel != permlevel: description = 'Page: %s, Changed from %s to %s' % ( page.name, oldpermlevel, permlevel) ModAction.create(c.site, c.user, 'wikipermlevel', description=description) return self.GET_wiki_settings(page=page.name)
def GET_wiki_create(self, wp, page): api = c.render_style in extensions.API_TYPES error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: error = error.msg_params if wp[0]: VNotInTimeout().run(action_name="wikirevise", details_text="create", target=page) return self.redirect(join_urls(c.wiki_base_url, wp[0].name)) elif api: if error: self.handle_error(403, **error) else: self.handle_error(404, 'PAGE_NOT_CREATED') elif error: error_msg = '' if error['reason'] == 'PAGE_NAME_LENGTH': error_msg = _( "this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters" ) % error['max_length'] elif error['reason'] == 'PAGE_CREATED_ELSEWHERE': error_msg = _( "this page is a special page, please go into the subreddit settings and save the field once to create this special page" ) elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS': error_msg = _( 'a max of %d separators "/" are allowed in a wiki page name.' ) % error['max_separators'] return BoringPage(_("Wiki error"), infotext=error_msg).render() else: VNotInTimeout().run(action_name="wikirevise", details_text="create") return WikiCreate(page=page, may_revise=True).render()
def POST_wiki_allow_editor(self, act, page, user): """Allow/deny `username` to edit this wiki `page`""" if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': VNotInTimeout().run(action_name="wikipermlevel", details_text="del_editor", target=user) page.remove_editor(user._id36) elif act == 'add': VNotInTimeout().run(action_name="wikipermlevel", details_text="allow_editor", target=user) page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({})
def POST_wiki_revision_revert(self, pv): """Revert a wiki `page` to `revision`""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') VNotInTimeout().run(action_name="wikirevise", details_text="revision_revert", target=page) content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content) if css_errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.name in ATTRIBUTE_BY_PAGE: 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 POST_wiki_revision_hide(self, pv): """Toggle the public visibility of a wiki page revision""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') VNotInTimeout().run(action_name="wikirevise", details_text="revision_hide", target=page) return json.dumps({'status': revision.toggle_hide()})
def GET_advertising(self): VNotInTimeout().run(action_name="pageview", details_text="advertising") subreddit_links = self._get_selfserve_links(3) content = Advertising(subreddit_links=subreddit_links, ) return AdvertisingPage( "advertise", content=content, loginbox=False, header=False, ).render()
def GET_wiki_settings(self, page): """Retrieve the current permission settings for `page`""" settings = {'permlevel': page._get('permlevel', 0), 'listed': page.listed} VNotInTimeout().run(action_name="pageview", details_text="wikisettings", target=page) mayedit = page.get_editor_accounts() restricted = (not page.special) and page.restricted show_editors = not restricted return WikiSettings(settings, mayedit, show_settings=not page.special, page=page.name, show_editors=show_editors, restricted=restricted, may_revise=True).render()
def GET_wiki_revise(self, wp, page, message=None, **kw): wp = wp[0] VNotInTimeout().run(action_name="wikirevise", details_text="revise", target=wp) error = c.errors.get(('MAY_NOT_REVISE', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) previous = kw.get('previous', wp._get('revision')) content = kw.get('content', wp.content) if not message and wp.name in page_descriptions: message = page_descriptions[wp.name] return WikiEdit(content, previous, alert=message, page=wp.name, may_revise=True).render()
def pre(self): # Set user_is_admin property on context, # normally set but this controller does not inherit # from RedditController super(ModmailController, self).pre() admin_usernames = [ name.lower() for name in g.live_config['modmail_admins'] ] c.user_is_admin = False if c.user_is_loggedin: c.user_is_admin = c.user.name.lower() in admin_usernames VNotInTimeout().run()
def POST_gild(self, target): if not isinstance(target, (Comment, Link)): err = RedditError("NO_THING_ID") self.on_validation_error(err) if target.subreddit_slow.quarantine: err = RedditError("GILDING_NOT_ALLOWED") self.on_validation_error(err) VNotInTimeout().run(target=target, subreddit=target.subreddit_slow) self._gift_using_creddits( recipient=target.author_slow, thing_fullname=target._fullname, proxying_for=request.POST.get("proxying_for"), )
def POST_wiki_edit(self, pageandprevious, content, page_name, reason): """Edit a wiki `page`""" page, previous = pageandprevious if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) VNotInTimeout().run(action_name="wikirevise", details_text="create") page = WikiPage.create(c.site, page_name) else: VNotInTimeout().run(action_name="wikirevise", details_text="edit", target=page) error = c.errors.get(('MAY_NOT_REVISE', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') if renderer in ('wiki', 'reddit'): content = VMarkdown(('content'), renderer=renderer).run(content) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.POST.get('previous') try: # special validation methods if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content, verify=False) if g.css_killswitch: self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if css_errors: error_items = [CssError(x).message for x in css_errors] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) elif page.name == "config/automoderator": try: rules = Ruleset(content) except ValueError as e: error_items = [e.message] self.handle_error(415, "SPECIAL_ERRORS", special_errors=error_items) # special saving methods if page.name == "config/stylesheet": c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special and page.name in ATTRIBUTE_BY_PAGE: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, "wikirevise", details=description, description=reason) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({})
class RobinController(RedditController): def pre(self): RedditController.pre(self) if not feature.is_enabled("robin"): self.abort404() @validate( VUser(), VNotInTimeout(), ) def GET_join(self): room = RobinRoom.get_room_for_user(c.user) if room: return self.redirect("/robin") return RobinPage( title="robin", content=RobinJoin( robin_heavy_load=g.live_config.get('robin_heavy_load')), ).render() @validate( VAdmin(), ) def GET_all(self): return RobinPage( title="robin", content=RobinAll(), ).render() @validate( VAdmin(), ) def GET_admin(self): return RobinPage( title="robin", content=RobinAdmin(), ).render() @validate( VUser(), VNotInTimeout(), ) def GET_chat(self): room = RobinRoom.get_room_for_user(c.user) if not room: return self.redirect("/robin/join") return self._get_chat_page(room) @validate( VAdmin(), room=VRobinRoom("room_id", allow_admin=True), ) def GET_force_room(self, room): """Allow admins to view a specific room""" return self._get_chat_page(room) @validate( VAdmin(), user=VAccountByName("user"), ) def GET_user_room(self, user): """Redirect admins to a user's room""" room = RobinRoom.get_room_for_user(user) if not room: self.abort404() self.redirect("/robin/" + room.id) def _get_chat_page(self, room): path = posixpath.join("/robin", room.id, c.user._id36) websocket_url = websockets.make_url(path, max_age=3600) all_user_ids = room.get_all_participants() all_present_ids = room.get_present_participants() all_votes = room.get_all_votes() users = Account._byID(all_user_ids, data=True, stale=True) user_list = [] for user in users.itervalues(): if user._id in all_votes: vote = all_votes.get(user._id) else: vote = None user_list.append({ "name": user.name, "present": user._id in all_present_ids, "vote": vote, }) return RobinChatPage( title="chat in %s" % room.name, content=RobinChat(room=room), extra_js_config={ "robin_room_is_continued": room.is_continued, "robin_room_name": room.name, "robin_room_id": room.id, "robin_websocket_url": websocket_url, "robin_user_list": user_list, "robin_room_date": js_timestamp(room.date), "robin_room_reap_time": js_timestamp(get_reap_time(room)), }, ).render() def _has_exceeded_ratelimit(self, form, room): # grab the ratelimit (as average events per second) for the room's # current level, using the highest level configured that's not bigger # than the room. e.g. if ratelimits are defined for levels 1, 2, and 4 # and the room is level 3, this will give us the ratelimit specified # for 2. desired_avg_per_sec = 1 by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {}) for level, avg_per_sec in sorted(by_level.items(), key=lambda (x, y): int(x)): if int(level) > room.level: break desired_avg_per_sec = avg_per_sec # now figure out how many events per window that means window_size = g.live_config.get("robin_ratelimit_window", 10) allowed_events_per_window = int(desired_avg_per_sec * window_size) try: # now figure out how much they've actually used ratelimit_key = "robin/{}".format(c.user._id36) time_slice = ratelimit.get_timeslice(window_size) usage = ratelimit.get_usage(ratelimit_key, time_slice) # ratelimit them if too much if usage >= allowed_events_per_window: g.stats.simple_event("robin.ratelimit.exceeded") period_end = datetime.datetime.utcfromtimestamp(time_slice.end) period_end_utc = period_end.replace(tzinfo=pytz.UTC) until_reset = utils.timeuntil(period_end_utc) c.errors.add(errors.RATELIMIT, {"time": until_reset}, field="ratelimit", code=429) form.has_errors("ratelimit", errors.RATELIMIT) return True # or record the usage and move on ratelimit.record_usage(ratelimit_key, time_slice) except ratelimit.RatelimitError as exc: g.log.warning("ratelimit error: %s", exc) return False @validatedForm( VUser(), VNotInTimeout(), VModhash(), room=VRobinRoom("room_id"), message=VLength("message", max_length=140), # TODO: do we want md? ) def POST_message(self, form, jquery, room, message): if self._has_exceeded_ratelimit(form, room): return if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG): return websockets.send_broadcast( namespace="/robin/" + room.id, type="chat", payload={ "from": c.user.name, "body": message, }, ) events.message( room=room, message=message, sent_dt=datetime.datetime.utcnow(), context=c, request=request, ) @validatedForm( VUser(), VNotInTimeout(), VModhash(), room=VRobinRoom("room_id"), vote=VOneOf("vote", VALID_VOTES), ) def POST_vote(self, form, jquery, room, vote): if self._has_exceeded_ratelimit(form, room): return if not vote: # TODO: error return? return g.stats.simple_event('robin.vote.%s' % vote) room.set_vote(c.user, vote) websockets.send_broadcast( namespace="/robin/" + room.id, type="vote", payload={ "from": c.user.name, "vote": vote, }, ) events.vote( room=room, vote=vote, sent_dt=datetime.datetime.utcnow(), context=c, request=request, ) @validatedForm( VUser(), VNotInTimeout(), VModhash(), ) def POST_join_room(self, form, jquery): if g.live_config.get('robin_heavy_load'): request.environ["usable_error_content"] = ( "Robin is currently experience high load.") abort(503) room = RobinRoom.get_room_for_user(c.user) if room: # user is already in a room, they should get redirected by the # frontend after polling /api/room_assignment.json return add_to_waitinglist(c.user) @validatedForm( VUser(), VModhash(), ) def POST_leave_room(self, form, jquery): room = RobinRoom.get_room_for_user(c.user) if not room: return room.remove_participants([c.user]) websockets.send_broadcast( namespace="/robin/" + room.id, type="users_abandoned", payload={ "users": [c.user.name], }, ) @json_validate( VUser(), VNotInTimeout(), ) def GET_room_assignment(self, responder): room = RobinRoom.get_room_for_user(c.user) if room: return {"roomId": room.id} @validatedForm( VAdmin(), VModhash(), ) def POST_admin_prompt(self, form, jquery): prompt_for_voting() @validatedForm( VAdmin(), VModhash(), ) def POST_admin_reap(self, form, jquery): reap_ripe_rooms() @validatedForm( VAdmin(), VModhash(), message=VLength("message", max_length=140), ) def POST_admin_broadcast(self, form, jquery, message): if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG): return websockets.send_broadcast( namespace="/robin", type="system_broadcast", payload={ "body": message, }, )
class APIv1GoldController(OAuth2OnlyController): def _gift_using_creddits(self, recipient, months=1, thing_fullname=None, proxying_for=None): with creddits_lock(c.user): if not c.user.employee and c.user.gold_creddits < months: err = RedditError("INSUFFICIENT_CREDDITS") self.on_validation_error(err) note = None buyer = c.user if c.user.name.lower() in g.live_config["proxy_gilding_accounts"]: note = "proxy-%s" % c.user.name if proxying_for: try: buyer = Account._by_name(proxying_for) except NotFound: pass send_gift( buyer=buyer, recipient=recipient, months=months, days=months * 31, signed=False, giftmessage=None, thing_fullname=thing_fullname, note=note, ) if not c.user.employee: c.user.gold_creddits -= months c.user._commit() @require_oauth2_scope("creddits") @validate( VUser(), target=VByName("fullname"), ) @api_doc( api_section.gold, uri="/api/v1/gold/gild/{fullname}", ) def POST_gild(self, target): if not isinstance(target, (Comment, Link)): err = RedditError("NO_THING_ID") self.on_validation_error(err) if target.subreddit_slow.quarantine: err = RedditError("GILDING_NOT_ALLOWED") self.on_validation_error(err) VNotInTimeout().run(target=target, subreddit=target.subreddit_slow) self._gift_using_creddits( recipient=target.author_slow, thing_fullname=target._fullname, proxying_for=request.POST.get("proxying_for"), ) @require_oauth2_scope("creddits") @validate( VUser(), user=VAccountByName("username"), months=VInt("months", min=1, max=36), timeout=VNotInTimeout(), ) @api_doc( api_section.gold, uri="/api/v1/gold/give/{username}", ) def POST_give(self, user, months, timeout): self._gift_using_creddits( recipient=user, months=months, proxying_for=request.POST.get("proxying_for"), )