# -*- coding: iso-8859-1 -*- # # Copyright (C) 2003-2006 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 import os import re import StringIO from trac.attachment import attachments_to_hdf from trac.core import * from trac.perm import IPermissionRequestor from trac.Search import ISearchSource, search_to_sql, shorten_result from trac.Timeline import ITimelineEventProvider from trac.util import get_reporter_id from trac.util.datefmt import format_datetime, pretty_timedelta from trac.util.html import html, Markup from trac.util.text import shorten_line, to_unicode from trac.versioncontrol.diff import get_diff_options, hdf_diff from trac.web.chrome import add_link, add_stylesheet, add_script from trac.web.chrome import INavigationContributor, ITemplateProvider from trac.web import HTTPNotFound, IRequestHandler from trac.wiki.api import IWikiPageManipulator, WikiSystem from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner from trac.mimeview.api import Mimeview, IContentConverter from WikiTemplates.model import WikiTemplate from WikiTemplates.errors import TemplatesError from WikiTemplates.attachment import Attachment, AttachmentModule from ctxtnavadd.api import ICtxtnavAdder class InvalidTemplatePage(TracError): """Exception raised when a Wiki page fails validation.""" class WikiTemplatesModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITimelineEventProvider, ISearchSource, ITemplateProvider, ICtxtnavAdder) page_manipulators = ExtensionPoint(IWikiPageManipulator) # ITemplateProvider methods def get_htdocs_dirs(self): from pkg_resources import resource_filename resource_dir = resource_filename(__name__, 'htdocs') return [('templates', resource_dir)] def get_templates_dirs(self): from pkg_resources import resource_filename resource_dir = resource_filename(__name__, 'templates') return [resource_dir] # INavigationContributor methods def get_active_navigation_item(self, req): return 'templates' def get_navigation_items(self, req): evil_js = '/'.join(['templates','js','wikitemplates.js']) add_script(req, evil_js) if not req.perm.has_permission('TEMPLATES_VIEW') or not \ req.perm.has_permission('TEMPLATES_ADMIN') or not \ self.env.is_component_enabled( 'wikitemplates.macros.templates.templatesmacro'): return yield ('mainnav', 'templates', html.A('Wiki Templates', href=req.href.templates(), accesskey=9, id='templatesbtn')) # ICtxtnavAdder methods def match_ctxtnav_add(self, req): if not req.perm.has_permission('TEMPLATES_VIEW') or not \ self.env.is_component_enabled( 'wikitemplates.macros.templates.templatesmacro'): return False self._add_js(req,self._make_js(req)) return len(req.path_info) <= 1 or req.path_info == '/' or \ req.path_info.startswith('/wiki') or \ req.path_info.startswith('/templates') def get_ctxtnav_adds(self, req): yield (req.href.templates(), 'Wiki Templates Index') # IPermissionRequestor methods def get_permission_actions(self): actions = [ 'TEMPLATES_CREATE', 'TEMPLATES_DELETE', 'TEMPLATES_MODIFY', 'TEMPLATES_VIEW' ] return actions + [('TEMPLATES_ADMIN', actions)] # IRequestHandler methods def match_request(self, req): match = re.match(r'^/templates(?:/(.*))?', req.path_info) if match: if match.group(1): req.args['page'] = match.group(1) self.env.log.debug("Page Match: '%s'", match.group(1)) self.env.log.debug("Page Match req.args: '%s'", to_unicode(req.args['page'])) else: req.args['page'] = 'TemplatesIndex' self.env.log.debug("Index Match: '%s'", to_unicode(req.args['page'])) return True def process_request(self, req): if req.method == 'POST': action = req.args.get('action', 'create') if action == 'create': redir = req.args.get('new_tpl_name') req.args['action'] = 'edit' req.redirect(self.env.href.templates(redir)) action = req.args.get('action', 'view') pagename = req.args.get('page', 'TemplatesIndex') version = req.args.get('version') db = self.env.get_db_cnx() cursor = db.cursor() req.hdf['templates_base_url'] = self.env.href.templates() # Query our DB for the template previews if pagename == 'TemplatesIndex': # Include all latest versions of every template # to show on the templates index QUERY = """SELECT name, text from templates WHERE version=(SELECT max(t2.version) FROM templates t2 WHERE templates.name=t2.name) ORDER BY name ASC""" cursor.execute(QUERY) req.hdf['templates.showform'] = 1 else: # Include only the latest verion of the template # being called. QUERY = """SELECT name, text FROM templates WHERE name=%s ORDER BY version DESC LIMIT 1""" cursor.execute(QUERY, (pagename,)) templates = cursor.fetchall() previews = [] # REGEX for our variables defenitions {{n}}, n being a digit args_re = re.compile(r'\{\{\d+\}\}') try: for template in templates: name = template[0] contents = template[1] # Dont include the TemplatesIndex if name != 'TemplatesIndex': try: # Substitute the var defenitions found on template # by bogus var to get a "real" preview for i in range(1, len(args_re.findall(contents)) + 1): contents = contents.replace('{{%d}}' % i, 'argument_%d' % i) self.env.log.debug('Replace Results: "%s"' % contents) except: self.env.log.debug('No variables replacing needed.') # Append the dict to the previews list previews.append({'name': name, 'contents': wiki_to_html(contents, self.env, req)}) # Assign our previews list to trac HDF req.hdf['previews'] = previews self.env.log.debug('TemplatesIndex List: "%s"' % previews) except Exception, e: # Aparently no entry was found on db, maybe it's a new template, # so, we don't include any preview, of course # self.env.log.debug("No previous record of '%s' found on DB.") self.env.log.debug(e) self.env.log.debug('Template Name: %s', pagename) # Continue normal wiki behaviour if pagename.endswith('/'): req.redirect(req.href.templates(pagename.strip('/'))) db = self.env.get_db_cnx() page = WikiTemplate(self.env, pagename, version, db) add_stylesheet(req, 'common/css/wiki.css') add_stylesheet(req, 'templates/css/wikitemplates.css') if req.method == 'POST': if action == 'edit': latest_version = WikiTemplate( self.env, pagename, None, db).version if req.args.has_key('cancel'): req.redirect(req.href.templates(page.name)) elif int(version) != latest_version: action = 'collision' self._render_editor(req, db, page) elif req.args.has_key('preview'): action = 'preview' self._render_editor(req, db, page, preview=True) else: self._do_save(req, db, page) elif action == 'delete': self._do_delete(req, db, page) elif action == 'diff': get_diff_options(req) req.redirect(req.href.templates( page.name, version=page.version, old_version=req.args.get('old_version'), action='diff')) elif action == 'delete': self._render_confirm(req, db, page) elif action == 'edit': self._render_editor(req, db, page) elif action == 'diff': self._render_diff(req, db, page) elif action == 'history': self._render_history(req, db, page) else: format = req.args.get('format') if format: Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', page.text, format, page.name) self._render_view(req, db, page) req.hdf['templates.action'] = action req.hdf['templates.current_href'] = req.href.templates(page.name) return 'WikiTemplates.cs', None # ITimelineEventProvider methods def get_timeline_filters(self, req): if req.perm.has_permission('TEMPLATES_VIEW'): yield ('templates', 'Wiki Templates Changes') def get_timeline_events(self, req, start, stop, filters): if 'templates' in filters: template = WikiSystem(self.env) format = req.args.get('format') href = format == 'rss' and req.abs_href or req.href db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT time,name,comment,author,version " "FROM templates WHERE time>=%s AND time<=%s", (start, stop)) for t,name,comment,author,version in cursor: title = Markup('%s wiki template edited by %s', template.format_page_name(name), author) diff_link = html.A('diff', href=href.templates( name, action='diff', version=version)) if format == 'rss': comment = wiki_to_html(comment or '--', self.env, req, db, absurls=True) else: comment = wiki_to_oneliner(comment, self.env, db, shorten=True) if version > 1: comment = Markup('%s (%s)', comment, diff_link) yield 'templates', href.templates(name), title, t, author, comment # Attachments def display(id): return Markup('ticket ', html.EM('#', id)) att = AttachmentModule(self.env) for event in att.get_timeline_events(req, db, 'templates', format, start, stop, lambda id: html.EM(id)): yield event # Internal methods def _set_title(self, req, page, action): title = name = WikiSystem(self.env).format_page_name(page.name) if action: title += ' (%s)' % action req.hdf['templates.page_name'] = name req.hdf['title'] = title return title def _do_delete(self, req, db, page): if page.readonly: req.perm.assert_permission('TEMPLATES_ADMIN') else: req.perm.assert_permission('TEMPLATES_DELETE') if req.args.has_key('cancel'): req.redirect(req.href.templates(page.name)) version = int(req.args.get('version', 0)) or None old_version = int(req.args.get('old_version', 0)) or version 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.templates()) else: req.redirect(req.href.templates(page.name)) def _do_save(self, req, db, page): if page.readonly: req.perm.assert_permission('TEMPLATES_ADMIN') elif not page.exists: req.perm.assert_permission('TEMPLATES_CREATE') else: req.perm.assert_permission('TEMPLATES_MODIFY') page.text = req.args.get('text') if req.perm.has_permission('TEMPLATES_ADMIN'): # Modify the read-only flag if it has been changed and the user is # TEMPLATES_ADMIN page.readonly = int(req.args.has_key('readonly')) # 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): if field: raise InvalidTemplatePage("The Template page field %s is invalid: %s" % (field, message)) else: raise InvalidTemplatePage("Invalid Template page: %s" % message) page.save(get_reporter_id(req, 'author'), req.args.get('comment'), req.remote_addr) req.redirect(req.href.templates(page.name)) def _render_confirm(self, req, db, page): if page.readonly: req.perm.assert_permission('TEMPLATES_ADMIN') else: req.perm.assert_permission('TEMPLATES_DELETE') version = None if req.args.has_key('delete_version'): version = int(req.args.get('version', 0)) old_version = int(req.args.get('old_version') or 0) or version self._set_title(req, page, 'delete') req.hdf['templates'] = {'mode': 'delete'} if version is not None: num_versions = 0 for v,t,author,comment,ipnr in page.get_history(): if v >= old_version: num_versions += 1; if num_versions > 1: break req.hdf['templates'] = { 'version': version, 'old_version': old_version, 'only_version': num_versions == 1 } def _render_diff(self, req, db, page): req.perm.assert_permission('TEMPLATES_VIEW') if not page.exists: raise TracError("Version %s of template %s does not exist" % (req.args.get('version'), page.name)) add_stylesheet(req, 'common/css/diff.css') self._set_title(req, page, 'diff') # Ask web spiders to not index old versions req.hdf['html.norobots'] = 1 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 = page.version, \ WikiTemplate(self.env, page.name, old_version) latest_page = WikiTemplate(self.env, page.name) new_version = int(page.version) info = { 'version': new_version, 'latest_version': latest_page.version, 'history_href': req.href.templates(page.name, action='history') } num_changes = 0 old_page = None prev_version = next_version = None for version,t,author,comment,ipnr in latest_page.get_history(): if version == new_version: if t: info['time'] = format_datetime(t) info['time_delta'] = pretty_timedelta(t) info['author'] = author or 'anonymous' info['comment'] = wiki_to_html(comment or '--', self.env, req, db) info['ipnr'] = ipnr 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_page = WikiTemplate(self.env, page.name, version) info['num_changes'] = num_changes info['old_version'] = version break else: next_version = version req.hdf['templates'] = info # -- prev/next links if prev_version: add_link(req, 'prev', req.href.templates(page.name, action='diff', version=prev_version), 'Version %d' % prev_version) if next_version: add_link(req, 'next', req.href.templates(page.name, action='diff', version=next_version), 'Version %d' % next_version) # -- text diffs diff_style, diff_options = get_diff_options(req) oldtext = old_page and old_page.text.splitlines() or [] newtext = page.text.splitlines() context = 3 for option in diff_options: if option.startswith('-U'): context = int(option[2:]) break if context < 0: context = None changes = hdf_diff(oldtext, newtext, context=context, ignore_blank_lines='-B' in diff_options, ignore_case='-i' in diff_options, ignore_space_changes='-b' in diff_options) req.hdf['templates.diff'] = changes def _render_editor(self, req, db, page, preview=False): req.perm.assert_permission('TEMPLATES_MODIFY') if req.args.has_key('text'): page.text = req.args.get('text') if preview: page.readonly = req.args.has_key('readonly') author = get_reporter_id(req, 'author') comment = req.args.get('comment', '') editrows = req.args.get('editrows') if editrows: pref = req.session.get('templates_editrows', '20') if editrows != pref: req.session['templates_editrows'] = editrows else: editrows = req.session.get('templates_editrows', '20') self._set_title(req, page, 'edit') info = { 'page_source': page.text, 'version': page.version, 'author': author, 'comment': comment, 'readonly': page.readonly, 'edit_rows': editrows, 'scroll_bar_pos': req.args.get('scroll_bar_pos', '') } if page.exists: info['history_href'] = req.href.templates(page.name, action='history') info['last_change_href'] = req.href.templates(page.name, action='diff', version=page.version) if preview: info['page_html'] = wiki_to_html(page.text, self.env, req, db) info['comment_html'] = wiki_to_oneliner(comment, self.env, db) info['readonly'] = int(req.args.has_key('readonly')) req.hdf['templates'] = info def _render_history(self, req, db, page): """Extract the complete history for a given page and stores it in the HDF. This information is used to present a changelog/history for a given page. """ req.perm.assert_permission('TEMPLATES_VIEW') if not page.exists: raise TracError, "Template %s does not exist" % page.name self._set_title(req, page, 'history') history = [] for version, t, author, comment, ipnr in page.get_history(): history.append({ 'url': req.href.templates(page.name, version=version), 'diff_url': req.href.templates(page.name, version=version, action='diff'), 'version': version, 'time': format_datetime(t), 'time_delta': pretty_timedelta(t), 'author': author, 'comment': wiki_to_oneliner(comment or '', self.env, db), 'ipaddr': ipnr }) req.hdf['templates.history'] = history def _render_view(self, req, db, page): req.perm.assert_permission('TEMPLATES_VIEW') page_name = self._set_title(req, page, '') if page.name == 'TemplatesIndex': req.hdf['title'] = 'Wiki Templates Index' version = req.args.get('version') if version: # Ask web spiders to not index old versions req.hdf['html.norobots'] = 1 # Add registered converters for conversion in Mimeview(self.env).get_supported_conversions( 'text/x-trac-wiki'): conversion_href = req.href.templates(page.name, version=version, format=conversion[0]) add_link(req, 'alternate', conversion_href, conversion[1], conversion[3]) latest_page = WikiTemplate(self.env, page.name) req.hdf['templates'] = {'exists': page.exists, 'version': page.version, 'latest_version': latest_page.version, 'readonly': page.readonly} if page.exists: req.hdf['templates'] = { 'page_html': wiki_to_html(page.text, self.env, req), 'history_href': req.href.templates(page.name, action='history'), 'last_change_href': req.href.templates(page.name, action='diff', version=page.version) } if version: req.hdf['templates'] = { 'comment_html': wiki_to_oneliner(page.comment or '--', self.env, db), 'author': page.author, 'age': pretty_timedelta(page.time) } else: if not req.perm.has_permission('TEMPLATES_CREATE'): raise HTTPNotFound('Template %s not found', page.name) req.hdf['templates.page_html'] = html.P( 'Describe "%s" template here' % page_name) # Show attachments req.hdf['templates.attachments'] = attachments_to_hdf(self.env, req, db, 'templates', page.name) if req.perm.has_permission('TEMPLATES_MODIFY'): attach_href = req.href.attachment('templates', page.name) req.hdf['templates.attach_href'] = attach_href # TracCtxtnavAdd borrowed Internal Methods def _add_js(self, req, data): """Add javascript to a page via hdf['project.footer']""" footer = req.hdf['project.footer'] footer += data req.hdf['project.footer'] = Markup(footer) def _add_js_inc(self, req, file): """Add a javascript include via hdf['project.footer']""" self._add_js( req, """""" % file) def _make_js(self, req): return """""" # ISearchSource methods def get_search_filters(self, req): if req.perm.has_permission('TEMPLATES_VIEW'): yield ('templates', 'Wiki Templates') def get_search_results(self, req, terms, filters): if not 'templates' 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 templates w1," "(SELECT name,max(version) AS ver " "FROM templates GROUP BY name) w2 " "WHERE w1.version = w2.ver AND w1.name = w2.name " "AND " + sql_query, args) for name, date, author, text in cursor: yield (req.href.templates(name), '%s: %s' % (name, shorten_line(text)), date, author, shorten_result(text, terms))