| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (c) 2006-2010 Noah Kantrowitz <noah@coderanger.net> |
|---|
| 4 | # Copyright (c) 2013 Olemis Lang <olemis+trac@gmail.com> |
|---|
| 5 | # Copyright (c) 2021 Cinc |
|---|
| 6 | # |
|---|
| 7 | # All rights reserved. |
|---|
| 8 | # |
|---|
| 9 | # This software is licensed as described in the file COPYING, which |
|---|
| 10 | # you should have received as part of this distribution. |
|---|
| 11 | # |
|---|
| 12 | |
|---|
| 13 | import os.path |
|---|
| 14 | import sys |
|---|
| 15 | |
|---|
| 16 | from pkg_resources import resource_filename |
|---|
| 17 | |
|---|
| 18 | from trac.core import * |
|---|
| 19 | from trac.core import ComponentMeta |
|---|
| 20 | from trac.config import BoolOption |
|---|
| 21 | from trac.util.text import exception_to_unicode |
|---|
| 22 | from trac.web.chrome import add_script, add_stylesheet, add_warning, Chrome, \ |
|---|
| 23 | ITemplateProvider |
|---|
| 24 | from trac.web.api import IRequestFilter |
|---|
| 25 | |
|---|
| 26 | from themeengine.api import ThemeEngineSystem, ThemeNotFound |
|---|
| 27 | from themeengine.translation import I18N_DOC_OPTIONS |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | PY3 = sys.version_info.major == 3 |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | class ThemeEngineModule(Component): |
|---|
| 34 | """A module to provide the theme content.""" |
|---|
| 35 | |
|---|
| 36 | custom_css = BoolOption('theme', 'enable_css', default='false', |
|---|
| 37 | doc='Enable or disable custom CSS from theme.', |
|---|
| 38 | **I18N_DOC_OPTIONS) |
|---|
| 39 | |
|---|
| 40 | implements(ITemplateProvider, IRequestFilter) |
|---|
| 41 | |
|---|
| 42 | def __init__(self): |
|---|
| 43 | self.system = ThemeEngineSystem(self.env) |
|---|
| 44 | |
|---|
| 45 | # ITemplateProvider methods |
|---|
| 46 | def get_htdocs_dirs(self): |
|---|
| 47 | try: |
|---|
| 48 | theme = self.system.theme |
|---|
| 49 | if theme and 'htdocs' in theme: |
|---|
| 50 | theme_htdocs = theme['htdocs'] |
|---|
| 51 | if not os.path.isabs(theme_htdocs): |
|---|
| 52 | theme_htdocs = resource_filename(theme['module'], theme_htdocs) |
|---|
| 53 | yield 'theme', theme_htdocs |
|---|
| 54 | except ThemeNotFound: |
|---|
| 55 | pass |
|---|
| 56 | |
|---|
| 57 | def get_templates_dirs(self): |
|---|
| 58 | try: |
|---|
| 59 | theme = self.system.theme |
|---|
| 60 | if theme: |
|---|
| 61 | if 'template' in theme: |
|---|
| 62 | theme_templates = os.path.dirname(theme['template']) |
|---|
| 63 | if not os.path.isabs(theme_templates): |
|---|
| 64 | theme_templates = resource_filename(theme['module'], |
|---|
| 65 | theme_templates) |
|---|
| 66 | yield theme_templates |
|---|
| 67 | if 'jinja_template' in theme: |
|---|
| 68 | theme_templates = os.path.dirname(theme['jinja_template']) |
|---|
| 69 | if not os.path.isabs(theme_templates): |
|---|
| 70 | theme_templates = resource_filename(theme['module'], |
|---|
| 71 | theme_templates) |
|---|
| 72 | yield theme_templates |
|---|
| 73 | |
|---|
| 74 | except ThemeNotFound: |
|---|
| 75 | pass |
|---|
| 76 | |
|---|
| 77 | # IRequestFilter methods |
|---|
| 78 | def pre_process_request(self, req, handler): |
|---|
| 79 | return handler |
|---|
| 80 | |
|---|
| 81 | def post_process_request(self, req, template, data, content_type): |
|---|
| 82 | if (template, data) != (None, None) or \ |
|---|
| 83 | sys.exc_info() != (None, None, None): |
|---|
| 84 | try: |
|---|
| 85 | theme = self.system.theme |
|---|
| 86 | except ThemeNotFound as e: |
|---|
| 87 | add_warning(req, "Unknown theme %s configured. Please check " |
|---|
| 88 | "your trac.ini. You may need to enable " |
|---|
| 89 | "the theme\'s plugin." % e.theme_name) |
|---|
| 90 | else: |
|---|
| 91 | if theme and 'css' in theme: |
|---|
| 92 | css = theme['css'] |
|---|
| 93 | if isinstance(css, tuple) or isinstance(css, list): |
|---|
| 94 | for css_file in css: |
|---|
| 95 | add_stylesheet(req, 'theme/' + css_file) |
|---|
| 96 | else: |
|---|
| 97 | add_stylesheet(req, 'theme/' + theme['css']) |
|---|
| 98 | if hasattr(Chrome, 'jenv'): |
|---|
| 99 | # Trac 1.4 supports Genshi and Jinja2 templates. content_type is 'None' in Trac 1.4 |
|---|
| 100 | # when a Genshi template is being rendered. |
|---|
| 101 | # Trac 1.6 only supports Jinja2. |
|---|
| 102 | if content_type != None or (template, data) == (None, None): |
|---|
| 103 | # If an exception occurs, for example permission error, the content_type is always 'None'. |
|---|
| 104 | # So we need to handle exception pages here. |
|---|
| 105 | if theme and 'jinja_template' in theme: |
|---|
| 106 | req.chrome['theme'] = os.path.basename(theme['jinja_template']) |
|---|
| 107 | else: |
|---|
| 108 | # Legacy Genshi template rendering. |
|---|
| 109 | if theme and 'template' in theme: |
|---|
| 110 | req.chrome['theme'] = os.path.basename(theme['template']) |
|---|
| 111 | else: |
|---|
| 112 | if theme and 'template' in theme: |
|---|
| 113 | req.chrome['theme'] = os.path.basename(theme['template']) |
|---|
| 114 | if theme and 'scripts' in theme: |
|---|
| 115 | for script_def in theme['scripts']: |
|---|
| 116 | if (isinstance(script_def, tuple) and |
|---|
| 117 | 1 <= len(script_def) <= 4): |
|---|
| 118 | temp = [item for item in script_def] |
|---|
| 119 | if not temp[0].startswith('theme'): |
|---|
| 120 | temp[0] = 'theme/' + temp[0].lstrip('/') |
|---|
| 121 | add_script(req, *temp) |
|---|
| 122 | else: |
|---|
| 123 | self.log.warning('Bad script def %s for theme %s. Is definition a tuple?', |
|---|
| 124 | script_def, theme['name']) |
|---|
| 125 | if theme and theme.get('disable_trac_css'): |
|---|
| 126 | links = req.chrome.get('links') |
|---|
| 127 | if links and 'stylesheet' in links: |
|---|
| 128 | for i, link in enumerate(links['stylesheet']): |
|---|
| 129 | if link.get('href', '') \ |
|---|
| 130 | .endswith('common/css/trac.css'): |
|---|
| 131 | del links['stylesheet'][i] |
|---|
| 132 | break |
|---|
| 133 | if theme: |
|---|
| 134 | req.chrome['theme_info'] = theme |
|---|
| 135 | # Template overrides (since 2.2.0) |
|---|
| 136 | overrides = self._get_template_overrides(theme) |
|---|
| 137 | template, modifier = overrides.get(template, |
|---|
| 138 | (template, None)) |
|---|
| 139 | if modifier is not None: |
|---|
| 140 | modifier(req, template, data, content_type) |
|---|
| 141 | if self.custom_css: |
|---|
| 142 | add_stylesheet(req, 'site/theme.css') |
|---|
| 143 | |
|---|
| 144 | return template, data, content_type |
|---|
| 145 | |
|---|
| 146 | # Protected methods |
|---|
| 147 | def _get_template_overrides(self, theme): |
|---|
| 148 | overrides = theme.get('template_overrides') |
|---|
| 149 | if overrides is None: |
|---|
| 150 | try: |
|---|
| 151 | overrides = theme['provider'].get_template_overrides( |
|---|
| 152 | theme['name']) |
|---|
| 153 | except Exception as e: |
|---|
| 154 | overrides = {} |
|---|
| 155 | self.log.warning('Theme %s template overrides : %s', |
|---|
| 156 | theme['name'], |
|---|
| 157 | exception_to_unicode(e)) |
|---|
| 158 | else: |
|---|
| 159 | overrides = dict([old, (new, callback)] |
|---|
| 160 | for old, new, callback in overrides) |
|---|
| 161 | theme['template_overrides'] = overrides |
|---|
| 162 | return overrides |
|---|