class WikiController(RedditController): allow_stylesheets = True @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/{page}', uses_site=True) @validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'), required=False, restricted=False, allow_hidden_revision=False), page_name=VWikiPageName('page', error_on_name_normalized=True)) def GET_wiki_page(self, pv, page_name): """Return the content of a wiki page If `v` is given, show the wiki page as it was at that version If both `v` and `v2` are given, show a diff of the two """ message = None if c.errors.get(('PAGE_NAME_NORMALIZED', 'page')): url = join_urls(c.wiki_base_url, page_name) return self.redirect(url) page, version, version2 = pv if not page: is_api = c.render_style in extensions.API_TYPES if this_may_revise(): if is_api: self.handle_error(404, 'PAGE_NOT_CREATED') errorpage = WikiNotFound(page=page_name) request.environ['usable_error_content'] = errorpage.render() elif is_api: self.handle_error(404, 'PAGE_NOT_FOUND') self.abort404() if version: edit_by = version.get_author() edit_date = version.date else: edit_by = page.get_author() edit_date = page._get('last_edit_date') diffcontent = None if not version: content = page.content if c.is_wiki_mod and page.name in page_descriptions: message = page_descriptions[page.name] else: message = _("viewing revision from %s") % timesince(version.date) if version2: t1 = timesince(version.date) t2 = timesince(version2.date) timestamp1 = _("%s ago") % t1 timestamp2 = _("%s ago") % t2 message = _("comparing revisions from %(date_1)s and %(date_2)s") \ % {'date_1': t1, 'date_2': t2} diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2) content = version2.content else: message = _("viewing revision from %s ago") % timesince( version.date) content = version.content renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') return WikiPageView(content, alert=message, v=version, diff=diffcontent, may_revise=this_may_revise(page), edit_by=edit_by, edit_date=edit_date, page=page.name, renderer=renderer).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions/{page}', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') @validate(page=VWikiPage(('page'), restricted=False)) def GET_wiki_revisions(self, num, after, reverse, count, page): """Retrieve a list of revisions of this wiki `page`""" revisions = page.get_revisions() wikiuser = c.user if c.user_is_loggedin else None builder = WikiRevisionBuilder(revisions, user=wikiuser, sr=c.site, num=num, reverse=reverse, count=count, after=after, skip=not c.is_wiki_mod, wrap=default_thing_wrapper(), page=page) listing = WikiRevisionListing(builder).listing() return WikiRevisions(listing, page=page.name, may_revise=this_may_revise(page)).render() @validate(wp=VWikiPageRevise('page'), page=VWikiPageName('page')) def GET_wiki_create(self, wp, page): api = c.render_style in extensions.API_TYPES error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: error = error.msg_params if wp[0]: return self.redirect(join_urls(c.wiki_base_url, wp[0].name)) elif api: if error: self.handle_error(403, **error) else: self.handle_error(404, 'PAGE_NOT_CREATED') elif error: error_msg = '' if error['reason'] == 'PAGE_NAME_LENGTH': error_msg = _( "this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters" ) % error['max_length'] elif error['reason'] == 'PAGE_CREATED_ELSEWHERE': error_msg = _( "this page is a special page, please go into the subreddit settings and save the field once to create this special page" ) elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS': error_msg = _( 'a max of %d separators "/" are allowed in a wiki page name.' ) % error['max_separators'] return BoringPage(_("Wiki error"), infotext=error_msg).render() else: return WikiCreate(page=page, may_revise=True).render() @validate(wp=VWikiPageRevise('page', restricted=True, required=True)) def GET_wiki_revise(self, wp, page, message=None, **kw): wp = wp[0] previous = kw.get('previous', wp._get('revision')) content = kw.get('content', wp.content) if not message and wp.name in page_descriptions: message = page_descriptions[wp.name] return WikiEdit(content, previous, alert=message, page=wp.name, may_revise=True).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') def GET_wiki_recent(self, num, after, reverse, count): """Retrieve a list of recently changed wiki pages in this subreddit""" revisions = WikiRevision.get_recent(c.site) wikiuser = c.user if c.user_is_loggedin else None builder = WikiRecentRevisionBuilder(revisions, num=num, count=count, reverse=reverse, after=after, wrap=default_thing_wrapper(), skip=not c.is_wiki_mod, user=wikiuser, sr=c.site) listing = WikiRevisionListing(builder).listing() return WikiRecent(listing).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/pages', uses_site=True) def GET_wiki_listing(self): """Retrieve a list of wiki pages in this subreddit""" def check_hidden(page): return page.listed and this_may_view(page) pages, linear_pages = WikiPage.get_listing(c.site, filter_check=check_hidden) return WikiListing(pages, linear_pages).render() def GET_wiki_redirect(self, page='index'): return self.redirect(str("%s/%s" % (c.wiki_base_url, page)), code=301) @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/discussions/{page}', uses_site=True) @base_listing @validate(page=VWikiPage('page', restricted=True)) def GET_wiki_discussions(self, page, num, after, reverse, count): """Retrieve a list of discussions about this wiki `page`""" page_url = add_sr("%s/%s" % (c.wiki_base_url, page.name)) builder = url_links_builder(page_url, num=num, after=after, reverse=reverse, count=count) listing = LinkListing(builder).listing() return WikiDiscussions(listing, page=page.name, may_revise=this_may_revise(page)).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate(page=VWikiPage('page', restricted=True, modonly=True)) def GET_wiki_settings(self, page): """Retrieve the current permission settings for `page`""" settings = { 'permlevel': page._get('permlevel', 0), 'listed': page.listed } mayedit = page.get_editor_accounts() restricted = (not page.special) and page.restricted show_editors = not restricted return WikiSettings(settings, mayedit, show_settings=not page.special, page=page.name, show_editors=show_editors, restricted=restricted, may_revise=True).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate(VModhash(), page=VWikiPage('page', restricted=True, modonly=True), permlevel=VInt('permlevel'), listed=VBoolean('listed')) def POST_wiki_settings(self, page, permlevel, listed): """Update the permissions and visibility of wiki `page`""" oldpermlevel = page.permlevel try: page.change_permlevel(permlevel) except ValueError: self.handle_error(403, 'INVALID_PERMLEVEL') if page.listed != listed: page.listed = listed page._commit() verb = 'Relisted' if listed else 'Delisted' description = '%s page %s' % (verb, page.name) ModAction.create(c.site, c.user, 'wikipagelisted', description=description) if oldpermlevel != permlevel: description = 'Page: %s, Changed from %s to %s' % ( page.name, oldpermlevel, permlevel) ModAction.create(c.site, c.user, 'wikipermlevel', description=description) return self.GET_wiki_settings(page=page.name) def on_validation_error(self, error): RedditController.on_validation_error(self, error) if error.code: self.handle_error(error.code, error.name) def handle_error(self, code, reason=None, **data): abort(reddit_http_error(code, reason, **data)) def pre(self): RedditController.pre(self) if g.disable_wiki and not c.user_is_admin: self.handle_error(403, 'WIKI_DOWN') if not c.site._should_wiki: self.handle_error(404, 'NOT_WIKIABLE') # /r/mod for an example frontpage = isinstance(c.site, DefaultSR) c.wiki_base_url = join_urls(c.site.path, 'wiki') c.wiki_api_url = join_urls(c.site.path, '/api/wiki') c.wiki_id = g.default_sr if frontpage else c.site.name self.editconflict = False c.is_wiki_mod = (c.user_is_admin or c.site.is_moderator_with_perms( c.user, 'wiki')) if c.user_is_loggedin else False c.wikidisabled = False mode = c.site.wikimode if not mode or mode == 'disabled': if not c.is_wiki_mod: self.handle_error(403, 'WIKI_DISABLED') else: c.wikidisabled = True # Redirects from the old wiki def GET_faq(self): return self.GET_wiki_redirect(page='faq') GET_help = GET_wiki_redirect
class WikiApiController(WikiController): @require_oauth2_scope("wikiedit") @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=nop(('content')), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256, empty_error=None)) @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True) def POST_wiki_edit(self, pageandprevious, content, page_name, reason): """Edit a wiki `page`""" page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') if renderer in ('wiki', 'reddit'): content = VMarkdown(('content'), renderer=renderer).run(content) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.POST.get('previous') try: # special validation methods if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content, verify=False) if g.css_killswitch: self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if css_errors: error_items = [CssError(x).message for x in css_errors] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) elif page.name == "config/automoderator": try: rules = Ruleset(content) except ValueError as e: error_items = [e.message] self.handle_error(415, "SPECIAL_ERRORS", special_errors=error_items) # special saving methods if page.name == "config/stylesheet": c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special and page.name in ATTRIBUTE_BY_PAGE: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, "wikirevise", details=description, description=reason) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/{act}', uses_site=True, uri_variants=[ '/api/wiki/alloweditor/%s' % act for act in ('del', 'add') ]) def POST_wiki_allow_editor(self, act, page, user): """Allow/deny `username` to edit this wiki `page`""" if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate( VModhash(), VAdmin(), pv=VWikiPageAndVersion(('page', 'revision')), deleted=VBoolean('deleted'), ) def POST_wiki_revision_delete(self, pv, deleted): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') if deleted and page.revision == str(revision._id): self.handle_error(400, 'REVISION_IS_CURRENT') revision.admin_deleted = deleted revision._commit() return json.dumps({'status': revision.admin_deleted}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True) def POST_wiki_revision_hide(self, pv): """Toggle the public visibility of a wiki page revision""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True) def POST_wiki_revision_revert(self, pv): """Revert a wiki `page` to `revision`""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content) if css_errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')
class WikiApiController(WikiController): @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=VMarkdown(('content'), renderer='wiki'), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256)) @api_doc(api_section.wiki, uri='/api/wiki/edit') def POST_wiki_edit(self, pageandprevious, content, page_name, reason): page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.post.get('previous') try: if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content, verify=False) if report is None: # g.css_killswitch self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if report.errors: error_items = [x.message for x in sorted(report.errors)] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", str(page.revision)) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, 'wikirevise', details=description) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/:act') def POST_wiki_allow_editor(self, act, page, user): if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide') def POST_wiki_revision_hide(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert') def POST_wiki_revision_revert(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content) if report.errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", page.revision) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')