edgewall/trac

View on GitHub
trac/wiki/web_ui.py

Summary

Maintainability
F
1 wk
Test Coverage
# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2023 Edgewall Software
# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
# 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 https://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 https://trac.edgewall.org/log/.
#
# Author: Jonas Borgström <jonas@edgewall.com>
#         Christopher Lenz <cmlenz@gmx.de>

import pkg_resources
import re

from trac.attachment import AttachmentModule, Attachment
from trac.config import IntOption
from trac.core import *
from trac.mimeview.api import IContentConverter, Mimeview
from trac.perm import IPermissionPolicy, 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 as_int, get_reporter_id
from trac.util.datefmt import from_utimestamp, to_utimestamp
from trac.util.html import tag
from trac.util.text import shorten_line
from trac.util.translation import _, tag_
from trac.versioncontrol.diff import get_diff_options, diff_blocks
from trac.web.api import HTTPBadRequest, IRequestHandler
from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider,
                             accesskey, add_ctxtnav, add_link,
                             add_notice, add_script, add_stylesheet,
                             add_warning, prevnext_nav, web_context)
from trac.wiki.api import IWikiPageManipulator, WikiSystem, validate_page_name
from trac.wiki.formatter import format_to, OneLinerFormatter
from trac.wiki.model import WikiPage


class WikiModule(Component):

    implements(IContentConverter, INavigationContributor,
               IPermissionRequestor, IRequestHandler, ITimelineEventProvider,
               ISearchSource, ITemplateProvider)

    page_manipulators = ExtensionPoint(IWikiPageManipulator)

    realm = WikiSystem.realm

    max_size = IntOption('wiki', 'max_size', 262144,
        """Maximum allowed wiki page size in characters.""")

    default_edit_area_height = IntOption('wiki', 'default_edit_area_height',
        20,
        """Default height of the textarea on the wiki edit page.
        (//Since 1.1.5//)""")

    START_PAGE = property(lambda self: WikiSystem.START_PAGE)
    TITLE_INDEX_PAGE = property(lambda self: WikiSystem.TITLE_INDEX_PAGE)
    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):
        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(self.realm, self.START_PAGE):
            yield ('mainnav', 'wiki',
                   tag.a(_("Wiki"), href=req.href.wiki(),
                         accesskey=accesskey(req, 1)))
        if 'WIKI_VIEW' in req.perm(self.realm, 'TracGuide'):
            yield ('metanav', 'help',
                   tag.a(_("Help/Guide"), href=req.href.wiki('TracGuide'),
                         accesskey=accesskey(req, 6)))

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_RENAME',
                   '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', self.START_PAGE)
        version = None
        if req.args.get('version'):  # Allow version to be empty
            version = req.args.getint('version')
        old_version = req.args.getint('old_version')

        if pagename.startswith('/') or pagename.endswith('/') or \
                '//' in pagename:
            pagename = re.sub(r'/{2,}', '/', pagename.strip('/'))
            req.redirect(req.href.wiki(pagename))
        if not validate_page_name(pagename):
            raise TracError(_("Invalid Wiki page name '%(name)s'",
                              name=pagename))

        page = WikiPage(self.env, pagename)
        versioned_page = WikiPage(self.env, pagename, version)

        req.perm(versioned_page.resource).require('WIKI_VIEW')

        if version and versioned_page.version != version:
            raise ResourceNotFound(
                _('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 = version != page.version
                for a in ('preview', 'diff', 'merge'):
                    if a in req.args:
                        action = a
                        break
                versioned_page.text = req.args.get('text')
                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 == 'edit_comment':
                self._do_edit_comment(req, versioned_page)
            elif action == 'delete':
                self._do_delete(req, versioned_page)
            elif action == 'rename':
                return self._do_rename(req, page)
            elif action == 'diff':
                style, options, diff_data = get_diff_options(req)
                contextall = diff_data['options']['contextall']
                req.redirect(req.href.wiki(versioned_page.name, action='diff',
                                           old_version=old_version,
                                           version=version,
                                           contextall=contextall or None))
            else:
                raise HTTPBadRequest(_("Invalid request arguments."))
        elif action == 'delete':
            return self._render_confirm_delete(req, page)
        elif action == 'rename':
            return self._render_confirm_rename(req, page)
        elif action == 'edit':
            return self._render_editor(req, page)
        elif action == 'edit_comment':
            return self._render_edit_comment(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, tag_("The Wiki page field %(field)s"
                                          " is invalid: %(message)s",
                                          field=tag.strong(field),
                                          message=message))
                else:
                    add_warning(req, tag_("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': req.href.wiki(page.name, version=v)
                            if v else 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_edit_comment(self, req, page):
        req.perm(page.resource).require('WIKI_ADMIN')

        redirect_to = req.args.get('redirect_to')
        version = old_version = None
        if redirect_to == 'diff':
            version = page.version
            old_version = version - 1
        redirect_href = req.href.wiki(page.name, action=redirect_to,
                                      version=version, old_version=old_version)
        if 'cancel' in req.args:
            req.redirect(redirect_href)

        new_comment = req.args.get('new_comment')

        page.edit_comment(new_comment)
        add_notice(req, _("The comment of version %(version)s of the page "
                          "%(name)s has been updated.",
                          version=page.version, name=page.name))
        req.redirect(redirect_href)

    def _do_delete(self, req, page):
        req.perm(page.resource).require('WIKI_DELETE')

        if 'cancel' in req.args:
            req.redirect(get_resource_url(self.env, page.resource, req.href))

        version = req.args.getint('version')
        old_version = req.args.getint('old_version', version)

        with self.env.db_transaction:
            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)
            else:
                # only delete that `version`, or the whole page if `None`
                page.delete(version)

        if not page.exists:
            add_notice(req, _("The page %(name)s has been deleted.",
                              name=page.name))
            req.redirect(req.href.wiki())
        else:
            if version and old_version and version > old_version + 1:
                add_notice(req, _("The versions %(from_)d to %(to)d of the "
                                  "page %(name)s have been deleted.",
                           from_=old_version + 1, to=version, name=page.name))
            else:
                add_notice(req, _("The version %(version)d of the page "
                                  "%(name)s has been deleted.",
                                  version=version, name=page.name))
            req.redirect(req.href.wiki(page.name))

    def _do_rename(self, req, page):
        req.perm(page.resource).require('WIKI_RENAME')

        if 'cancel' in req.args:
            req.redirect(get_resource_url(self.env, page.resource, req.href))

        old_name, old_version = page.name, page.version
        new_name = req.args.get('new_name', '')
        new_name = re.sub(r'/{2,}', '/', new_name.strip('/'))
        redirect = req.args.get('redirect')

        # verify input parameters
        warn = None
        if not new_name:
            warn = _("A new name is mandatory for a rename.")
        elif not validate_page_name(new_name):
            warn = _("The new name is invalid (a name which is separated "
                     "with slashes cannot be '.' or '..').")
        elif new_name == old_name:
            warn = _("The new name must be different from the old name.")
        elif WikiPage(self.env, new_name).exists:
            warn = _("The page %(name)s already exists.", name=new_name)
        if warn:
            add_warning(req, warn)
            return self._render_confirm_rename(req, page, new_name)

        with self.env.db_transaction as db:
            page.rename(new_name)
            if redirect:
                redirection = WikiPage(self.env, old_name)
                redirection.text = _('See [wiki:"%(name)s"].', name=new_name)
                author = get_reporter_id(req)
                comment = '[wiki:"%s@%d" %s] \u2192 [wiki:"%s"].' % (
                          new_name, old_version, old_name, new_name)
                redirection.save(author, comment)

        add_notice(req, _("The page %(old_name)s has been renamed to "
                          "%(new_name)s.", old_name=old_name,
                          new_name=new_name))
        if redirect:
            add_notice(req, _("The page %(old_name)s has been recreated "
                              "with a redirect to %(new_name)s.",
                              old_name=old_name, new_name=new_name))

        req.redirect(req.href.wiki(old_name if redirect else new_name))

    def _do_save(self, req, page):
        if not page.exists:
            req.perm(page.resource).require('WIKI_CREATE')
        else:
            req.perm(page.resource).require('WIKI_MODIFY')

        if 'WIKI_CHANGE_READONLY' 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'))
        except TracError:
            add_warning(req, _("Page not modified, showing latest version."))
            return self._render_view(req, page)

        href = req.href.wiki(page.name, action='diff', version=page.version)
        add_notice(req, tag_("Your changes have been saved in version "
                             "%(version)s (%(diff)s).", version=page.version,
                             diff=tag.a(_("diff"), href=href)))
        req.redirect(get_resource_url(self.env, page.resource, req.href,
                                      version=None))

    def _render_confirm_delete(self, req, page):
        req.perm(page.resource).require('WIKI_DELETE')

        version = None
        if 'delete_version' in req.args:
            version = req.args.getint('version', 0)
        old_version = req.args.getint('old_version', version)

        what = 'multiple' if version and old_version \
                             and version - old_version > 1 \
               else 'single' if version else 'page'

        num_versions = 0
        new_date = None
        old_date = None
        for v, t, author, comment in page.get_history():
            if (what == 'page' or v <= version) and new_date is None:
                new_date = t
            if (what == 'multiple' and v <= old_version or
                what == 'single' and num_versions > 1):
                break
            num_versions += 1
            old_date = t

        data = self._page_data(req, page, 'delete')
        attachments = Attachment.select(self.env, self.realm, page.name)
        data.update({
            'what': what, 'new_version': None, 'old_version': None,
            'num_versions': num_versions, 'new_date': new_date,
            'old_date': old_date, 'attachments': list(attachments),
        })
        if version is not None:
            data.update({'new_version': version, 'old_version': old_version})
        self._wiki_ctxtnav(req, page)
        return 'wiki_delete.html', data

    def _render_confirm_rename(self, req, page, new_name=None):
        req.perm(page.resource).require('WIKI_RENAME')

        data = self._page_data(req, page, 'rename')
        data['new_name'] = new_name if new_name is not None else page.name
        self._wiki_ctxtnav(req, page)
        return 'wiki_rename.html', data

    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.getint('old_version')
        if 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, old_version)
                req.perm(page.resource).require('WIKI_VIEW')
        latest_page = WikiPage(self.env, page.name)
        req.perm(latest_page.resource).require('WIKI_VIEW')
        new_version = page.version

        date = author = comment = None
        num_changes = 0
        prev_version = next_version = None
        for version, t, a, c in latest_page.get_history():
            if version == new_version:
                date = t
                author = a or 'anonymous'
                comment = c or '--'
            else:
                if version < new_version:
                    num_changes += 1
                    if not prev_version:
                        prev_version = version
                    if old_version is None or version == old_version:
                        old_version = version
                        break
                else:
                    next_version = version
        if not old_version:
            old_version = 0
        old_page = WikiPage(self.env, page.name, old_version)
        req.perm(old_page.resource).require('WIKI_VIEW')

        # -- text diffs
        old_text = old_page.text.splitlines()
        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, '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,
            'can_edit_comment': 'WIKI_ADMIN' in req.perm(page.resource),
        })
        prevnext_nav(req, _("Previous Change"), _("Next Change"),
                     _("Wiki History"))
        return 'wiki_diff.html', data

    def _render_editor(self, req, page, action='edit', has_collision=False):
        if has_collision:
            if action == 'merge':
                page = WikiPage(self.env, page.name)
                req.perm(page.resource).require('WIKI_VIEW')
            else:
                action = 'collision'

        if not page.exists:
            req.perm(page.resource).require('WIKI_CREATE')
        else:
            req.perm(page.resource).require('WIKI_MODIFY')
        original_text = page.text
        comment = req.args.get('comment', '')
        if 'text' in req.args:
            page.text = req.args.get('text')
        elif 'template' in req.args:
            template = req.args.get('template')
            template = template[1:] if template.startswith('/') \
                                    else self.PAGE_TEMPLATES_PREFIX + 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
        elif 'version' in req.args:
            version = None
            if req.args.get('version'):  # Allow version to be empty
                version = req.args.as_int('version')
            if version is not None:
                old_page = WikiPage(self.env, page.name, version)
                req.perm(page.resource).require('WIKI_VIEW')
                page.text = old_page.text
                comment = _("Reverted to version %(version)s.",
                            version=version)
        if action in ('preview', 'diff'):
            page.readonly = 'readonly' in req.args

        author = get_reporter_id(req, 'author')
        defaults = {'editrows': str(self.default_edit_area_height)}
        prefs = {key: req.session.get('wiki_%s' % key, defaults.get(key))
                 for key in ('editrows', 'sidebyside')}

        if 'from_editor' in req.args:
            sidebyside = req.args.get('sidebyside') or None
            if sidebyside != prefs['sidebyside']:
                req.session.set('wiki_sidebyside', int(bool(sidebyside)), 0)
        else:
            sidebyside = prefs['sidebyside']

        if sidebyside:
            editrows = max(int(prefs['editrows']),
                           len(page.text.splitlines()) + 1)
        else:
            editrows = req.args.get('editrows')
            if editrows:
                if editrows != prefs['editrows']:
                    req.session.set('wiki_editrows', editrows,
                                    defaults['editrows'])
            else:
                editrows = prefs['editrows']

        data = self._page_data(req, page, action)
        context = web_context(req, page.resource)
        data.update({
            'context': context,
            'author': author,
            'comment': comment,
            'edit_rows': editrows,
            'sidebyside': sidebyside,
            'scroll_bar_pos': req.args.get('scroll_bar_pos', ''),
            'diff': None,
            'attachments': AttachmentModule(self.env).attachment_data(context)
        })
        if action in ('diff', 'merge'):
            old_text = original_text.splitlines() if original_text else []
            new_text = page.text.splitlines() if page.text else []
            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'})
        elif sidebyside and action != 'collision':
            data['action'] = 'preview'

        self._wiki_ctxtnav(req, page)
        Chrome(self.env).add_wiki_toolbars(req)
        Chrome(self.env).add_auto_preview(req)
        add_script(req, 'common/js/wiki.js')
        return 'wiki_edit.html', data

    def _render_edit_comment(self, req, page):
        req.perm(page.resource).require('WIKI_ADMIN')
        data = self._page_data(req, page, 'edit_comment')
        data.update({'redirect_to': req.args.get('redirect_to', 'history')})
        self._wiki_ctxtnav(req, page)
        return 'wiki_edit_comment.html', data

    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 in page.get_history():
            history.append({
                'version': version,
                'date': date,
                'author': author,
                'comment': comment or ''
            })
        data.update({
            'history': history,
            'resource': page.resource,
            'can_edit_comment': 'WIKI_ADMIN' in req.perm(page.resource)
        })
        add_ctxtnav(req, _("Back to %(wikipage)s", wikipage=page.name),
                    req.href.wiki(page.name))
        return 'history_view.html', data

    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.key)
                add_link(req, 'alternate', conversion_href, conversion.name,
                         conversion.in_mimetype)

        data = self._page_data(req, page)
        if page.name == self.START_PAGE:
            data['title'] = ''

        ws = WikiSystem(self.env)
        context = web_context(req, page.resource)
        higher, related = [], []
        if not page.exists:
            if 'WIKI_CREATE' not in req.perm(page.resource):
                raise ResourceNotFound(_("Page %(name)s not found",
                                         name=page.name))
            formatter = OneLinerFormatter(self.env, context)
            if '/' in page.name:
                parts = page.name.split('/')
                for i in range(len(parts) - 2, -1, -1):
                    name = '/'.join(parts[:i] + [parts[-1]])
                    if not ws.has_page(name):
                        higher.append(ws._format_link(formatter, 'wiki',
                                                      '/' + name, name, False))
            else:
                name = page.name
            name = name.lower()
            related = [each for each in ws.pages
                       if name in each.lower()
                          and 'WIKI_VIEW' in req.perm(self.realm, each)]
            related.sort()
            related = [ws._format_link(formatter, 'wiki', '/' + each, each,
                                       False)
                       for each in related]

        latest_page = WikiPage(self.env, page.name)

        prev_version = next_version = None
        if version:
            version = as_int(version, None)
            if version is not None:
                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

        prefix = self.PAGE_TEMPLATES_PREFIX
        templates = [template[len(prefix):]
                     for template in ws.get_pages(prefix)
                     if 'WIKI_VIEW' in req.perm(self.realm, 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, _("Previous Version"), _("Next Version"),
                         _("View Latest Version"))
        else:
            if parent:
                add_ctxtnav(req, _('Up'), req.href.wiki(parent))
            self._wiki_ctxtnav(req, page)

        # Plugin content validation
        fields = {'text': page.text}
        for manipulator in self.page_manipulators:
            manipulator.prepare_wiki_page(req, page, fields)
        text = fields.get('text', '')

        data.update({
            'context': context,
            'text': text,
            'latest_version': latest_page.version,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'start_page': self.START_PAGE,
            'default_template': self.DEFAULT_PAGE_TEMPLATE,
            'templates': templates,
            'version': version,
            'higher': higher, 'related': related,
            'resourcepath_template': 'wiki_page_path.html',
            'fullwidth': req.session.get('wiki_fullwidth'),
        })
        add_script(req, 'common/js/wiki.js')
        return 'wiki_view.html', data

    def _wiki_ctxtnav(self, req, page):
        """Add the normal wiki ctxtnav entries."""
        if 'WIKI_VIEW' in req.perm('wiki', self.START_PAGE):
            add_ctxtnav(req, _("Start Page"), req.href.wiki(self.START_PAGE))
        if 'WIKI_VIEW' in req.perm('wiki', self.TITLE_INDEX_PAGE):
            add_ctxtnav(req, _("Index"), req.href.wiki(self.TITLE_INDEX_PAGE))
        if page.exists:
            add_ctxtnav(req, _("History"), req.href.wiki(page.name,
                                                         action='history'))

    # 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):
        if 'wiki' in filters:
            wiki_realm = Resource(self.realm)
            for ts, name, comment, author, version in self.env.db_query("""
                    SELECT time, name, comment, author, version FROM wiki
                    WHERE time>=%s AND time<=%s
                    """, (to_utimestamp(start), to_utimestamp(stop))):
                wiki_page = wiki_realm(id=name, version=version)
                if 'WIKI_VIEW' not in req.perm(wiki_page):
                    continue
                yield ('wiki', from_utimestamp(ts), 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':
            name = tag.em(get_resource_name(self.env, wiki_page))
            if wiki_page.version > 1:
                return tag_("%(page)s edited", page=name)
            else:
                return tag_("%(page)s created", page=name)
        elif field == 'description':
            markup = format_to(self.env, None,
                               context.child(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 'wiki' not in filters:
            return
        with self.env.db_query as db:
            sql_query, args = search_to_sql(db, ['w1.name', 'w1.author',
                                                 'w1.text'], terms)
            wiki_realm = Resource(self.realm)
            for name, ts, author, text in db("""
                    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):
                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)),
                           from_utimestamp(ts), author,
                           shorten_result(text, terms))

        # Attachments
        for result in AttachmentModule(self.env).get_search_results(
                req, wiki_realm, terms):
            yield result


class DefaultWikiPolicy(Component):
    """Default permission policy for the wiki system.

    Wiki pages with the read-only attribute require `WIKI_ADMIN` to delete,
    modify or rename the page.
    """

    implements(IPermissionPolicy)

    realm = WikiSystem.realm

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        if resource and resource.realm == self.realm:
            if action == 'WIKI_CHANGE_READONLY':
                return 'WIKI_ADMIN' in perm(resource)
            if action in ('WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_RENAME'):
                page = WikiPage(self.env, resource)
                if page.readonly and 'WIKI_ADMIN' not in perm(resource):
                    return False