# -*- coding: utf-8 -*- # # Copyright (C) 2003-2008 Edgewall Software # Copyright (C) 2003-2005 Jonas Borgström # Copyright (C) 2004-2005 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Jonas Borgström # Christopher Lenz from datetime import datetime import pkg_resources import re from genshi.core import Markup from genshi.builder import tag from trac.attachment import AttachmentModule from trac.config import IntOption from trac.core import * from trac.mimeview.api import Mimeview, IContentConverter, Context from trac.perm import IPermissionRequestor from trac.resource import * from trac.search import ISearchSource, search_to_sql, shorten_result from trac.timeline.api import ITimelineEventProvider from trac.util import get_reporter_id from trac.util.datefmt import to_timestamp, utc from trac.util.text import shorten_line from trac.util.translation import _ from trac.versioncontrol.diff import get_diff_options, diff_blocks from trac.web.chrome import add_link, add_script, add_stylesheet, \ add_ctxtnav, add_warning, prevnext_nav, \ INavigationContributor, ITemplateProvider from trac.web import IRequestHandler from trac.wiki.api import IWikiPageManipulator, WikiSystem from trac.wiki.formatter import format_to from trac.wiki.model import WikiPage class InvalidWikiPage(TracError): """Exception raised when a Wiki page fails validation. :deprecated: Not used anymore since 0.11 """ class WikiModule(Component): implements(IContentConverter, INavigationContributor, IPermissionRequestor, IRequestHandler, ITimelineEventProvider, ISearchSource, ITemplateProvider) page_manipulators = ExtensionPoint(IWikiPageManipulator) max_size = IntOption('wiki', 'max_size', 262144, """Maximum allowed wiki page size in bytes. (''since 0.11.2'')""") PAGE_TEMPLATES_PREFIX = 'PageTemplates/' DEFAULT_PAGE_TEMPLATE = 'DefaultPage' # IContentConverter methods def get_supported_conversions(self): yield ('txt', _('Plain Text'), 'txt', 'text/x-trac-wiki', 'text/plain', 9) def convert_content(self, req, mimetype, content, key): # Tell the browser that the content should be downloaded and # not rendered. The x=y part is needed to keep Safari from being # confused by the multiple content-disposition headers. req.send_header('Content-Disposition', 'attachment; x=y') return (content, 'text/plain;charset=utf-8') # INavigationContributor methods def get_active_navigation_item(self, req): return 'wiki' def get_navigation_items(self, req): if 'WIKI_VIEW' in req.perm('wiki'): yield ('mainnav', 'wiki', tag.a(_('Wiki'), href=req.href.wiki(), accesskey=1)) # IPermissionRequestor methods def get_permission_actions(self): actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_VIEW'] return actions + [('WIKI_ADMIN', actions)] # IRequestHandler methods def match_request(self, req): match = re.match(r'/wiki(?:/(.+))?$', req.path_info) if match: if match.group(1): req.args['page'] = match.group(1) return 1 def process_request(self, req): action = req.args.get('action', 'view') pagename = req.args.get('page', 'WikiStart') version = req.args.get('version') old_version = req.args.get('old_version') if pagename.endswith('/'): req.redirect(req.href.wiki(pagename.strip('/'))) page = WikiPage(self.env, pagename) versioned_page = WikiPage(self.env, pagename, version=version) req.perm(page.resource).require('WIKI_VIEW') req.perm(versioned_page.resource).require('WIKI_VIEW') if version and versioned_page.version == 0 and \ page.version != 0: raise TracError(_('No version "%(num)s" for Wiki page "%(name)s"', num=version, name=page.name)) add_stylesheet(req, 'common/css/wiki.css') if req.method == 'POST': if action == 'edit': if 'cancel' in req.args: req.redirect(req.href.wiki(page.name)) has_collision = int(version) != page.version for a in ('preview', 'diff', 'merge'): if a in req.args: action = a break valid = self._validate(req, versioned_page) if action == 'edit' and not has_collision and valid: return self._do_save(req, versioned_page) else: return self._render_editor(req, page, action, has_collision) elif action == 'delete': self._do_delete(req, versioned_page) elif action == 'diff': get_diff_options(req) req.redirect(req.href.wiki(versioned_page.name, action='diff', old_version=old_version)) elif action == 'delete': return self._render_confirm(req, versioned_page) elif action == 'edit': return self._render_editor(req, versioned_page) elif action == 'diff': return self._render_diff(req, versioned_page) elif action == 'history': return self._render_history(req, versioned_page) else: format = req.args.get('format') if format: Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', versioned_page.text, format, versioned_page.name) return self._render_view(req, versioned_page) # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): return [pkg_resources.resource_filename('trac.wiki', 'templates')] # Internal methods def _validate(self, req, page): valid = True # Validate page size if len(req.args.get('text', '')) > self.max_size: add_warning(req, _('The wiki page is too long (must be less ' 'than %(num)s characters)', num=self.max_size)) valid = False # Give the manipulators a pass at post-processing the page for manipulator in self.page_manipulators: for field, message in manipulator.validate_wiki_page(req, page): valid = False if field: add_warning(req, _("The Wiki page field '%(field)s' is " "invalid: %(message)s", field=field, message=message)) else: add_warning(req, _("Invalid Wiki page: %(message)s", message=message)) return valid def _page_data(self, req, page, action=''): title = get_resource_summary(self.env, page.resource) if action: title += ' (%s)' % action return {'page': page, 'action': action, 'title': title} def _prepare_diff(self, req, page, old_text, new_text, old_version, new_version): diff_style, diff_options, diff_data = get_diff_options(req) diff_context = 3 for option in diff_options: if option.startswith('-U'): diff_context = int(option[2:]) break if diff_context < 0: diff_context = None diffs = diff_blocks(old_text, new_text, context=diff_context, ignore_blank_lines='-B' in diff_options, ignore_case='-i' in diff_options, ignore_space_changes='-b' in diff_options) def version_info(v, last=0): return {'path': get_resource_name(self.env, page.resource), # TRANSLATOR: wiki page 'rev': v or _('currently edited'), 'shortrev': v or last + 1, 'href': v and req.href.wiki(page.name, version=v) or None} changes = [{'diffs': diffs, 'props': [], 'new': version_info(new_version, old_version), 'old': version_info(old_version)}] add_stylesheet(req, 'common/css/diff.css') add_script(req, 'common/js/diff.js') return diff_data, changes def _do_delete(self, req, page): if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') else: req.perm(page.resource).require('WIKI_DELETE') if 'cancel' in req.args: req.redirect(get_resource_url(self.env, page.resource, req.href)) version = int(req.args.get('version', 0)) or None old_version = int(req.args.get('old_version', 0)) or version db = self.env.get_db_cnx() if version and old_version and version > old_version: # delete from `old_version` exclusive to `version` inclusive: for v in range(old_version, version): page.delete(v + 1, db) else: # only delete that `version`, or the whole page if `None` page.delete(version, db) db.commit() if not page.exists: req.redirect(req.href.wiki()) else: req.redirect(req.href.wiki(page.name)) def _do_save(self, req, page): if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') elif not page.exists: req.perm(page.resource).require('WIKI_CREATE') else: req.perm(page.resource).require('WIKI_MODIFY') page.text = req.args.get('text') if 'WIKI_ADMIN' in req.perm(page.resource): # Modify the read-only flag if it has been changed and the user is # WIKI_ADMIN page.readonly = int('readonly' in req.args) try: page.save(get_reporter_id(req, 'author'), req.args.get('comment'), req.remote_addr) req.redirect(get_resource_url(self.env, page.resource, req.href, version=None)) except TracError: add_warning(req, _("Page not modified, showing latest version.")) return self._render_view(req, page) def _render_confirm(self, req, page): if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') else: req.perm(page.resource).require('WIKI_DELETE') version = None if 'delete_version' in req.args: version = int(req.args.get('version', 0)) old_version = int(req.args.get('old_version') or 0) or version data = self._page_data(req, page, 'delete') data.update({'new_version': None, 'old_version': None, 'num_versions': 0}) if version is not None: num_versions = 0 for v,t,author,comment,ipnr in page.get_history(): num_versions += 1; if num_versions > 1: break data.update({'new_version': version, 'old_version': old_version, 'num_versions': num_versions}) self._wiki_ctxtnav(req, page) return 'wiki_delete.html', data, None def _render_diff(self, req, page): if not page.exists: raise TracError(_('Version %(num)s of page "%(name)s" does not ' 'exist', num=req.args.get('version'), name=page.name)) old_version = req.args.get('old_version') if old_version: old_version = int(old_version) if old_version == page.version: old_version = None elif old_version > page.version: # FIXME: what about reverse diffs? old_version = page.resource.version page = WikiPage(self.env, page.name, version=old_version) req.perm(page.resource).require('WIKI_VIEW') latest_page = WikiPage(self.env, page.name, version=None) req.perm(latest_page.resource).require('WIKI_VIEW') new_version = int(page.version) date = author = comment = ipnr = None num_changes = 0 old_page = None prev_version = next_version = None for version, t, a, c, i in latest_page.get_history(): if version == new_version: date = t author = a or 'anonymous' comment = c or '--' ipnr = i or '' else: if version < new_version: num_changes += 1 if not prev_version: prev_version = version if (old_version and version == old_version) or \ not old_version: old_version = version old_page = WikiPage(self.env, page.name, old_version) req.perm(old_page.resource).require('WIKI_VIEW') break else: next_version = version if not old_version: old_version = 0 # -- text diffs old_text = old_page and old_page.text.splitlines() or [] new_text = page.text.splitlines() diff_data, changes = self._prepare_diff(req, page, old_text, new_text, old_version, new_version) # -- prev/up/next links if prev_version: add_link(req, 'prev', req.href.wiki(page.name, action='diff', version=prev_version), _('Version %(num)s', num=prev_version)) add_link(req, 'up', req.href.wiki(page.name, action='history'), _('Page history')) if next_version: add_link(req, 'next', req.href.wiki(page.name, action='diff', version=next_version), _('Version %(num)s', num=next_version)) data = self._page_data(req, page, 'diff') data.update({ 'change': {'date': date, 'author': author, 'ipnr': ipnr, 'comment': comment}, 'new_version': new_version, 'old_version': old_version, 'latest_version': latest_page.version, 'num_changes': num_changes, 'longcol': 'Version', 'shortcol': 'v', 'changes': changes, 'diff': diff_data, }) prevnext_nav(req, _('Change'), _('Wiki History')) return 'wiki_diff.html', data, None def _render_editor(self, req, page, action='edit', has_collision=False): if has_collision: if action == 'merge': page = WikiPage(self.env, page.name, version=None) req.perm(page.resource).require('WIKI_VIEW') else: action = 'collision' if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') else: req.perm(page.resource).require('WIKI_MODIFY') original_text = page.text if 'text' in req.args: page.text = req.args.get('text') elif 'template' in req.args: template = self.PAGE_TEMPLATES_PREFIX + req.args.get('template') template_page = WikiPage(self.env, template) if template_page and template_page.exists and \ 'WIKI_VIEW' in req.perm(template_page.resource): page.text = template_page.text if action == 'preview': page.readonly = 'readonly' in req.args author = get_reporter_id(req, 'author') comment = req.args.get('comment', '') editrows = req.args.get('editrows') if editrows: pref = req.session.get('wiki_editrows', '20') if editrows != pref: req.session['wiki_editrows'] = editrows else: editrows = req.session.get('wiki_editrows', '20') data = self._page_data(req, page, action) data.update({ 'author': author, 'comment': comment, 'edit_rows': editrows, 'scroll_bar_pos': req.args.get('scroll_bar_pos', ''), 'diff': None, }) if action in ('diff', 'merge'): old_text = original_text and original_text.splitlines() or [] new_text = page.text and page.text.splitlines() or [] diff_data, changes = self._prepare_diff( req, page, old_text, new_text, page.version, '') data.update({'diff': diff_data, 'changes': changes, 'action': 'preview', 'merge': action == 'merge', 'longcol': 'Version', 'shortcol': 'v'}) self._wiki_ctxtnav(req, page) return 'wiki_edit.html', data, None def _render_history(self, req, page): """Extract the complete history for a given page. This information is used to present a changelog/history for a given page. """ if not page.exists: raise TracError(_("Page %(name)s does not exist", name=page.name)) data = self._page_data(req, page, 'history') history = [] for version, date, author, comment, ipnr in page.get_history(): history.append({ 'version': version, 'date': date, 'author': author, 'comment': comment, 'ipnr': ipnr }) data.update({'history': history, 'resource': page.resource}) add_ctxtnav(req, 'Back to '+page.name, req.href.wiki(page.name)) return 'history_view.html', data, None def _render_view(self, req, page): version = page.resource.version # Add registered converters if page.exists: for conversion in Mimeview(self.env).get_supported_conversions( 'text/x-trac-wiki'): conversion_href = req.href.wiki(page.name, version=version, format=conversion[0]) # or... conversion_href = get_resource_url(self.env, page.resource, req.href, format=conversion[0]) add_link(req, 'alternate', conversion_href, conversion[1], conversion[3]) data = self._page_data(req, page) if page.name == 'WikiStart': data['title'] = '' if not page.exists: if 'WIKI_CREATE' not in req.perm(page.resource): raise ResourceNotFound(_('Page %(name)s not found', name=page.name)) latest_page = WikiPage(self.env, page.name, version=None) req.perm(latest_page.resource).require('WIKI_VIEW') prev_version = next_version = None if version: try: version = int(version) for hist in latest_page.get_history(): v = hist[0] if v != version: if v < version: if not prev_version: prev_version = v break else: next_version = v except ValueError: version = None prefix = self.PAGE_TEMPLATES_PREFIX templates = [template[len(prefix):] for template in WikiSystem(self.env).get_pages(prefix) if 'WIKI_VIEW' in req.perm('wiki', template)] # -- prev/up/next links if prev_version: add_link(req, 'prev', req.href.wiki(page.name, version=prev_version), _('Version %(num)s', num=prev_version)) parent = None if version: add_link(req, 'up', req.href.wiki(page.name, version=None), _('View latest version')) elif '/' in page.name: parent = page.name[:page.name.rindex('/')] add_link(req, 'up', req.href.wiki(parent, version=None), _("View parent page")) if next_version: add_link(req, 'next', req.href.wiki(page.name, version=next_version), _('Version %(num)s', num=next_version)) # Add ctxtnav entries if version: prevnext_nav(req, _('Version'), _('View Latest Version')) add_ctxtnav(req, _('Last Change'), req.href.wiki(page.name, action='diff', version=page.version)) else: if parent: add_ctxtnav(req, _('Up'), req.href.wiki(parent)) self._wiki_ctxtnav(req, page) context = Context.from_request(req, page.resource) data.update({ 'context': context, 'latest_version': latest_page.version, 'attachments': AttachmentModule(self.env).attachment_data(context), 'default_template': self.DEFAULT_PAGE_TEMPLATE, 'templates': templates, 'version': version }) return 'wiki_view.html', data, None def _wiki_ctxtnav(self, req, page): """Add the normal wiki ctxtnav entries.""" add_ctxtnav(req, _('Start Page'), req.href.wiki('WikiStart')) add_ctxtnav(req, _('Index'), req.href.wiki('TitleIndex')) if page.exists: add_ctxtnav(req, _('History'), req.href.wiki(page.name, action='history')) add_ctxtnav(req, _('Last Change'), req.href.wiki(page.name, action='diff', version=page.version)) # ITimelineEventProvider methods def get_timeline_filters(self, req): if 'WIKI_VIEW' in req.perm: yield ('wiki', _('Wiki changes')) def get_timeline_events(self, req, start, stop, filters): db = self.env.get_db_cnx() if 'wiki' in filters: wiki_realm = Resource('wiki') cursor = db.cursor() cursor.execute("SELECT time,name,comment,author,version " "FROM wiki WHERE time>=%s AND time<=%s", (to_timestamp(start), to_timestamp(stop))) for ts,name,comment,author,version in cursor: wiki_page = wiki_realm(id=name, version=version) if 'WIKI_VIEW' not in req.perm(wiki_page): continue yield ('wiki', datetime.fromtimestamp(ts, utc), author, (wiki_page, comment)) # Attachments for event in AttachmentModule(self.env).get_timeline_events( req, wiki_realm, start, stop): yield event def render_timeline_event(self, context, field, event): wiki_page, comment = event[3] if field == 'url': return context.href.wiki(wiki_page.id, version=wiki_page.version) elif field == 'title': return tag(tag.em(get_resource_name(self.env, wiki_page)), # TRANSLATOR: wiki page wiki_page.version > 1 and _(' edited') or _(' created')) elif field == 'description': markup = format_to(self.env, None, context(resource=wiki_page), comment) if wiki_page.version > 1: diff_href = context.href.wiki( wiki_page.id, version=wiki_page.version, action='diff') markup = tag(markup, ' ', tag.a('(diff)', href=diff_href)) return markup # ISearchSource methods def get_search_filters(self, req): if 'WIKI_VIEW' in req.perm: yield ('wiki', _('Wiki')) def get_search_results(self, req, terms, filters): if not 'wiki' in filters: return db = self.env.get_db_cnx() sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'], terms) cursor = db.cursor() cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text " "FROM wiki w1," "(SELECT name,max(version) AS ver " "FROM wiki GROUP BY name) w2 " "WHERE w1.version = w2.ver AND w1.name = w2.name " "AND " + sql_query, args) wiki_realm = Resource('wiki') for name, ts, author, text in cursor: page = wiki_realm(id=name) if 'WIKI_VIEW' in req.perm(page): yield (get_resource_url(self.env, page, req.href), '%s: %s' % (name, shorten_line(text)), datetime.fromtimestamp(ts, utc), author, shorten_result(text, terms)) # Attachments for result in AttachmentModule(self.env).get_search_results( req, wiki_realm, terms): yield result