# -*- coding: utf-8 -*- # # Copyright (C) 2005-2021 Edgewall Software # Copyright (C) 2005-2006 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 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: Christopher Lenz """Content presentation for the web layer. The Chrome module deals with delivering and shaping content to the end user, mostly targeting (X)HTML generation but not exclusively, RSS or other forms of web content are also using facilities provided here. """ from contextlib import contextmanager import datetime import itertools import operator import os.path import pkg_resources import pprint import re from functools import partial from jinja2 import FileSystemLoader from trac.api import IEnvironmentSetupParticipant, ISystemInfoProvider from trac.config import * from trac.core import * from trac.mimeview.api import RenderingContext, get_mimetype from trac.perm import IPermissionRequestor from trac.resource import * from trac.util import as_bool, as_int, get_pkginfo, get_reporter_id, html, \ pathjoin, presentation, to_list, translation from trac.util.html import (Element, Markup, escape, plaintext, tag, to_fragment, valid_html_bytes) from trac.util.text import (exception_to_unicode, is_obfuscated, javascript_quote, jinja2env, obfuscate_email_address, pretty_size, shorten_line, to_js_string, to_unicode, unicode_quote_plus) from trac.util.datefmt import ( pretty_timedelta, datetime_now, format_datetime, format_date, format_time, from_utimestamp, http_date, utc, get_date_format_jquery_ui, is_24_hours, get_time_format_jquery_ui, user_time, get_month_names_jquery_ui, get_day_names_jquery_ui, get_timezone_list_jquery_ui, get_first_week_day_jquery_ui, get_timepicker_separator_jquery_ui, get_period_names_jquery_ui, localtz) from trac.util.translation import _, get_available_locales from trac.web.api import IRequestHandler, HTTPNotFound from trac.web.href import Href from trac.wiki import IWikiSyntaxProvider from trac.wiki.formatter import format_to, format_to_html, format_to_oneliner default_mainnav_order = ('wiki', 'timeline', 'roadmap', 'browser', 'tickets', 'newticket', 'search', 'admin') default_metanav_order = ('login', 'logout', 'prefs', 'help', 'about') class INavigationContributor(Interface): """Extension point interface for components that contribute items to the navigation. """ def get_active_navigation_item(req): """This method is only called for the `IRequestHandler` processing the request. It should return the name of the navigation item to be highlighted as active/current. """ def get_navigation_items(req): """Should return an iterable object over the list of navigation items to add, each being a tuple in the form (category, name, text). The category determines the location of the navigation item and can be `mainnav` or `metanav`. The name is a unique identifier that must match the string returned by get_active_navigation_item. The text is typically a link element with text that corresponds to the desired label for the navigation item, and an href. """ class ITemplateProvider(Interface): """Extension point interface for components that provide their own Jinja2 templates and/or accompanying static resources. """ def get_htdocs_dirs(): """Return a list of directories with static resources (such as style sheets, images, etc.) Each item in the list must be a `(prefix, abspath)` tuple. The `prefix` part defines the path in the URL that requests to these resources are prefixed with. The `abspath` is the absolute path to the directory containing the resources on the local file system. """ def get_templates_dirs(): """Return a list of directories containing the provided template files. """ def accesskey(req, key): """Helper function for creating accesskey HTML attribute according to preference values""" return key if req.session.as_int('accesskeys') else None def add_meta(req, content, http_equiv=None, name=None, scheme=None, lang=None): """Add a `` tag into the `` of the generated HTML.""" meta = {'content': content, 'http-equiv': http_equiv, 'name': name, 'scheme': scheme, 'lang': lang, 'xml:lang': lang} req.chrome.setdefault('metas', []).append(meta) def add_link(req, rel, href, title=None, mimetype=None, classname=None, **attrs): """Add a link to the chrome info that will be inserted as element in the of the generated HTML """ linkid = '%s:%s' % (rel, href) linkset = req.chrome.setdefault('linkset', set()) if linkid in linkset: return # Already added that link link = {'href': href, 'title': title, 'type': mimetype, 'class': classname} link.update(attrs) links = req.chrome.setdefault('links', {}) links.setdefault(rel, []).append(link) linkset.add(linkid) def add_stylesheet(req, filename, mimetype='text/css', **attrs): """Add a link to a style sheet to the chrome info so that it gets included in the generated HTML page. If `filename` is a network-path reference (i.e. starts with a protocol or `//`), the return value will not be modified. If `filename` is absolute (i.e. starts with `/`), the generated link will be based off the application root path. If it is relative, the link will be based off the `/chrome/` path. """ href = chrome_resource_path(req, filename) add_link(req, 'stylesheet', href, mimetype=mimetype, **attrs) def add_script(req, filename, mimetype='text/javascript'): """Add a reference to an external javascript file to the template. If `filename` is a network-path reference (i.e. starts with a protocol or `//`), the return value will not be modified. If `filename` is absolute (i.e. starts with `/`), the generated link will be based off the application root path. If it is relative, the link will be based off the `/chrome/` path. """ scriptset = req.chrome.setdefault('scriptset', set()) if filename in scriptset: return False # Already added that script href = chrome_resource_path(req, filename) script = {'attrs': {'src': href}} if mimetype != 'text/javascript' and mimetype is not None: script['attrs']['type'] = mimetype req.chrome.setdefault('scripts', []).append(script) scriptset.add(filename) def add_script_data(req, data={}, **kwargs): """Add data to be made available in javascript scripts as global variables. The keys in `data` and the keyword argument names provide the names of the global variables. The values are converted to JSON and assigned to the corresponding variables. """ script_data = req.chrome.setdefault('script_data', {}) script_data.update(data) script_data.update(kwargs) def add_warning(req, msg, *args): """Add a non-fatal warning to the request object. When rendering pages, all warnings will be rendered to the user. Note that the message is escaped (and therefore converted to `Markup`) before it is stored in the request object. """ _add_message(req, 'warnings', msg, args) def add_notice(req, msg, *args): """Add an informational notice to the request object. When rendering pages, all notices will be rendered to the user. Note that the message is escaped (and therefore converted to `Markup`) before it is stored in the request object. """ _add_message(req, 'notices', msg, args) def _add_message(req, name, msg, args): if args: msg %= args if not isinstance(msg, Markup): msg = Markup(to_fragment(msg)) if msg not in req.chrome[name]: req.chrome[name].append(msg) def add_ctxtnav(req, elm_or_label, href=None, title=None): """Add an entry to the current page's ctxtnav bar.""" if href: elm = tag.a(elm_or_label, href=href, title=title) else: elm = elm_or_label req.chrome.setdefault('ctxtnav', []).append(elm) def prevnext_nav(req, prev_label, next_label, up_label=None): """Add Previous/Up/Next navigation links. :param req: a `Request` object :param prev_label: the label to use for left (previous) link :param up_label: the label to use for the middle (up) link :param next_label: the label to use for right (next) link """ links = req.chrome['links'] prev_link = next_link = None if not any(lnk in links for lnk in ('prev', 'up', 'next')): # Short circuit return if 'prev' in links: prev = links['prev'][0] prev_link = tag.a(prev_label, href=prev['href'], title=prev['title'], class_='prev') add_ctxtnav(req, tag.span(Markup('← '), prev_link or prev_label, class_='missing' if not prev_link else None)) if up_label and 'up' in links: up = links['up'][0] add_ctxtnav(req, tag.a(up_label, href=up['href'], title=up['title'])) if 'next' in links: next_ = links['next'][0] next_link = tag.a(next_label, href=next_['href'], title=next_['title'], class_='next') add_ctxtnav(req, tag.span(next_link or next_label, Markup(' →'), class_='missing' if not next_link else None)) def web_context(req, resource=None, id=False, version=False, parent=False, absurls=False): """Create a rendering context from a request. The `perm` and `href` properties of the context will be initialized from the corresponding properties of the request object. >>> from trac.test import Mock, MockPerm >>> req = Mock(href=Mock(), perm=MockPerm()) >>> context = web_context(req) >>> context.href is req.href True >>> context.perm is req.perm True :param req: the HTTP request object :param resource: the `Resource` object or realm :param id: the resource identifier :param version: the resource version :param absurls: whether URLs generated by the ``href`` object should be absolute (including the protocol scheme and host name) :return: a new rendering context :rtype: `RenderingContext` :since: version 1.0 """ if req: href = req.abs_href if absurls else req.href perm = req.perm else: href = None perm = None self = RenderingContext(Resource(resource, id=id, version=version, parent=parent), href=href, perm=perm) self.req = req return self def auth_link(req, link): """Return an "authenticated" link to `link` for authenticated users. If the user is anonymous, returns `link` unchanged. For authenticated users, returns a link to `/login` that redirects to `link` after authentication. """ if req.is_authenticated: return req.href.login(referer=link) return link def chrome_info_script(req, use_late=None): """Get script elements from chrome info of the request object during rendering template or after rendering. :param req: the HTTP request object. :param use_late: if True, `late_links` will be used instead of `links`. """ chrome = req.chrome if use_late: links = chrome.get('late_links', {}).get('stylesheet', []) scripts = chrome.get('late_scripts', []) script_data = chrome.get('late_script_data', {}) else: links = chrome.get('early_links', {}).get('stylesheet', []) + \ chrome.get('links', {}).get('stylesheet', []) scripts = chrome.get('early_scripts', []) + chrome.get('scripts', []) script_data = {} script_data.update(chrome.get('early_script_data', {})) script_data.update(chrome.get('script_data', {})) content = [] content.extend('jQuery.loadStyleSheet(%s, %s);' % (to_js_string(link['href']), to_js_string(link['type'])) for link in links or ()) content.extend('var %s=%s;' % (name, presentation.to_json(value)) for name, value in (script_data or {}).items()) fragment = tag() if content: fragment.append(tag.script('\n'.join(content), type='text/javascript')) for script in scripts: attrs = script['attrs'] fragment.append(tag.script( 'jQuery.loadScript(%s, %s)' % (to_js_string(attrs['src']), to_js_string(attrs.get('type'))))) return fragment def chrome_resource_path(req, filename): """Get the path for a chrome resource given its `filename`. If `filename` is a network-path reference (i.e. starts with a protocol or `//`), the return value will not be modified. If `filename` is absolute (i.e. starts with `/`), the generated link will be based off the application root path. If it is relative, the link will be based off the `/chrome/` path. """ if filename.startswith(('http://', 'https://', '//')): return filename elif filename.startswith('common/') and 'htdocs_location' in req.chrome: return Href(req.chrome['htdocs_location'])(filename[7:]) else: href = req.href if filename.startswith('/') else req.href.chrome return href(filename) def _save_messages(req, url, permanent): """Save warnings and notices in case of redirect, so that they can be displayed after the redirect.""" for type_ in ['warnings', 'notices']: for (i, message) in enumerate(req.chrome[type_]): req.session['chrome.%s.%d' % (type_, i)] = escape(message, False) @contextmanager def component_guard(env, req, component): """Traps any runtime exception raised when working with a component, logs the error and adds a warning for the user. """ with env.component_guard(component): try: yield except Exception as e: add_warning(req, _("%(component)s failed with %(exc)s", component=component.__class__.__name__, exc=exception_to_unicode(e))) raise class Chrome(Component): """Web site chrome assembly manager. Chrome is everything that is not actual page content. """ implements(ISystemInfoProvider, IEnvironmentSetupParticipant, IPermissionRequestor, IRequestHandler, ITemplateProvider, IWikiSyntaxProvider) required = True is_valid_default_handler = False navigation_contributors = ExtensionPoint(INavigationContributor) template_providers = ExtensionPoint(ITemplateProvider) shared_templates_dir = PathOption('inherit', 'templates_dir', '', """Path to the //shared templates directory//. Templates in that directory are loaded in addition to those in the environments `templates` directory, but the latter take precedence. Non-absolute paths are relative to the Environment `conf` directory. """) shared_htdocs_dir = PathOption('inherit', 'htdocs_dir', '', """Path to the //shared htdocs directory//. Static resources in that directory are mapped to /chrome/shared under the environment URL, in addition to common and site locations. This can be useful in site.html for common interface customization of multiple Trac environments. Non-absolute paths are relative to the Environment `conf` directory. (''since 1.0'')""") auto_reload = BoolOption('trac', 'auto_reload', False, """Automatically reload template files after modification.""") htdocs_location = Option('trac', 'htdocs_location', '', """Base URL for serving the core static resources below `/chrome/common/`. It can be left empty, and Trac will simply serve those resources itself. Advanced users can use this together with [TracAdmin trac-admin ... deploy ] to allow serving the static resources for Trac directly from the web server. Note however that this only applies to the `/htdocs/common` directory, the other deployed resources (i.e. those from plugins) will not be made available this way and additional rewrite rules will be needed in the web server.""") jquery_location = Option('trac', 'jquery_location', '', """Location of the jQuery !JavaScript library (version %(version)s). An empty value loads jQuery from the copy bundled with Trac. Alternatively, jQuery could be loaded from a CDN, for example: http://code.jquery.com/jquery-%(version)s.min.js, http://ajax.aspnetcdn.com/ajax/jQuery/jquery-%(version)s.min.js or https://ajax.googleapis.com/ajax/libs/jquery/%(version)s/jquery.min.js. (''since 1.0'')""", doc_args={'version': '1.12.4'}) jquery_ui_location = Option('trac', 'jquery_ui_location', '', """Location of the jQuery UI !JavaScript library (version %(version)s). An empty value loads jQuery UI from the copy bundled with Trac. Alternatively, jQuery UI could be loaded from a CDN, for example: https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/jquery-ui.min.js or http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/jquery-ui.min.js. (''since 1.0'')""", doc_args={'version': '1.12.1'}) jquery_ui_theme_location = Option('trac', 'jquery_ui_theme_location', '', """Location of the theme to be used with the jQuery UI !JavaScript library (version %(version)s). An empty value loads the custom Trac jQuery UI theme from the copy bundled with Trac. Alternatively, a jQuery UI theme could be loaded from a CDN, for example: https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/themes/start/jquery-ui.css or http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/themes/start/jquery-ui.css. (''since 1.0'')""", doc_args={'version': '1.12.1'}) mainnav = ConfigSection('mainnav', """Configures the main navigation bar, which by default contains //Wiki//, //Timeline//, //Roadmap//, //Browse Source//, //View Tickets//, //New Ticket//, //Search// and //Admin//. The `label`, `href`, and `order` attributes can be specified. Entries can be disabled by setting the value of the navigation item to `disabled`. The following example renames the link to WikiStart to //Home//, links the //View Tickets// entry to a specific report and disables the //Search// entry. {{{#!ini [mainnav] wiki.label = Home tickets.href = /report/24 search = disabled }}} See TracNavigation for more details. """) metanav = ConfigSection('metanav', """Configures the meta navigation entries, which by default are //Login//, //Logout//, //Preferences//, ''!Help/Guide'' and //About Trac//. The allowed attributes are the same as for `[mainnav]`. Additionally, a special entry is supported - `logout.redirect` is the page the user sees after hitting the logout button. For example: {{{#!ini [metanav] logout.redirect = wiki/Logout }}} See TracNavigation for more details. """) logo_link = Option('header_logo', 'link', '', """URL to link to, from the header logo.""") logo_src = Option('header_logo', 'src', 'site/your_project_logo.png', """URL of the image to use as header logo. It can be absolute, server relative or relative. If relative, it is relative to one of the `/chrome` locations: `site/your-logo.png` if `your-logo.png` is located in the `htdocs` folder within your TracEnvironment; `common/your-logo.png` if `your-logo.png` is located in the folder mapped to the [#trac-section htdocs_location] URL. Only specifying `your-logo.png` is equivalent to the latter.""") logo_alt = Option('header_logo', 'alt', "(please configure the [header_logo] section in trac.ini)", """Alternative text for the header logo.""") logo_width = IntOption('header_logo', 'width', -1, """Width of the header logo image in pixels.""") logo_height = IntOption('header_logo', 'height', -1, """Height of the header logo image in pixels.""") show_email_addresses = BoolOption('trac', 'show_email_addresses', 'false', """Show email addresses instead of usernames. If false, email addresses are obfuscated for users that don't have EMAIL_VIEW permission. """) show_full_names = BoolOption('trac', 'show_full_names', 'true', """Show full names instead of usernames. (//since 1.2//)""") never_obfuscate_mailto = BoolOption('trac', 'never_obfuscate_mailto', 'false', """Never obfuscate `mailto:` links explicitly written in the wiki, even if `show_email_addresses` is false or the user doesn't have EMAIL_VIEW permission. """) resizable_textareas = BoolOption('trac', 'resizable_textareas', 'true', """Make `