| 1 | # -*- coding: iso-8859-1 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2005 Edgewall Software |
|---|
| 4 | # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de> |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING, which |
|---|
| 8 | # you should have received as part of this distribution. The terms |
|---|
| 9 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 10 | # |
|---|
| 11 | # This software consists of voluntary contributions made by many |
|---|
| 12 | # individuals. For the exact contribution history, see the revision |
|---|
| 13 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 14 | # |
|---|
| 15 | # Author: Christopher Lenz <cmlenz@gmx.de> |
|---|
| 16 | |
|---|
| 17 | from __future__ import generators |
|---|
| 18 | import os.path |
|---|
| 19 | import re |
|---|
| 20 | |
|---|
| 21 | from trac import mimeview, util |
|---|
| 22 | from trac.core import * |
|---|
| 23 | from trac.env import IEnvironmentSetupParticipant |
|---|
| 24 | from trac.web.api import IRequestHandler |
|---|
| 25 | from trac.web.href import Href |
|---|
| 26 | |
|---|
| 27 | def add_link(req, rel, href, title=None, mimetype=None, classname=None): |
|---|
| 28 | """Add a link to the HDF data set that will be inserted as <link> element in |
|---|
| 29 | the <head> of the generated HTML |
|---|
| 30 | """ |
|---|
| 31 | link = {'href': href} |
|---|
| 32 | if title: |
|---|
| 33 | link['title'] = title |
|---|
| 34 | if mimetype: |
|---|
| 35 | link['type'] = mimetype |
|---|
| 36 | if classname: |
|---|
| 37 | link['class'] = classname |
|---|
| 38 | idx = 0 |
|---|
| 39 | while req.hdf.get('chrome.links.%s.%d.href' % (rel, idx)): |
|---|
| 40 | idx += 1 |
|---|
| 41 | req.hdf['chrome.links.%s.%d' % (rel, idx)] = link |
|---|
| 42 | |
|---|
| 43 | def add_stylesheet(req, filename, mimetype='text/css'): |
|---|
| 44 | """Add a link to a style sheet to the HDF data set so that it gets included |
|---|
| 45 | in the generated HTML page. |
|---|
| 46 | """ |
|---|
| 47 | if filename.startswith('common/') and 'htdocs_location' in req.hdf: |
|---|
| 48 | href = Href(req.hdf['htdocs_location']) |
|---|
| 49 | filename = filename[7:] |
|---|
| 50 | else: |
|---|
| 51 | href = Href(req.cgi_location).chrome |
|---|
| 52 | add_link(req, 'stylesheet', href(filename), mimetype=mimetype) |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | class INavigationContributor(Interface): |
|---|
| 56 | """Extension point interface for components that contribute items to the |
|---|
| 57 | navigation. |
|---|
| 58 | """ |
|---|
| 59 | |
|---|
| 60 | def get_active_navigation_item(req): |
|---|
| 61 | """This method is only called for the `IRequestHandler` processing the |
|---|
| 62 | request. |
|---|
| 63 | |
|---|
| 64 | It should return the name of the navigation item that should be |
|---|
| 65 | highlighted as active/current. |
|---|
| 66 | """ |
|---|
| 67 | |
|---|
| 68 | def get_navigation_items(req): |
|---|
| 69 | """Should return an iterable object over the list of navigation items to |
|---|
| 70 | add, each being a tuple in the form (category, name, text). |
|---|
| 71 | """ |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | class ITemplateProvider(Interface): |
|---|
| 75 | """Extension point interface for components that provide their own |
|---|
| 76 | ClearSilver templates and accompanying static resources. |
|---|
| 77 | """ |
|---|
| 78 | |
|---|
| 79 | def get_htdocs_dirs(): |
|---|
| 80 | """Return a list of directories with static resources (such as style |
|---|
| 81 | sheets, images, etc.) |
|---|
| 82 | |
|---|
| 83 | Each item in the list must be a `(prefix, abspath)` tuple. The |
|---|
| 84 | `prefix` part defines the path in the URL that requests to these |
|---|
| 85 | resources are prefixed with. |
|---|
| 86 | |
|---|
| 87 | The `abspath` is the absolute path to the directory containing the |
|---|
| 88 | resources on the local file system. |
|---|
| 89 | """ |
|---|
| 90 | |
|---|
| 91 | def get_templates_dirs(): |
|---|
| 92 | """Return a list of directories containing the provided ClearSilver |
|---|
| 93 | templates. |
|---|
| 94 | """ |
|---|
| 95 | |
|---|
| 96 | |
|---|
| 97 | class Chrome(Component): |
|---|
| 98 | """Responsible for assembling the web site chrome, i.e. everything that |
|---|
| 99 | is not actual page content. |
|---|
| 100 | """ |
|---|
| 101 | implements(IEnvironmentSetupParticipant, IRequestHandler, ITemplateProvider) |
|---|
| 102 | |
|---|
| 103 | navigation_contributors = ExtensionPoint(INavigationContributor) |
|---|
| 104 | template_providers = ExtensionPoint(ITemplateProvider) |
|---|
| 105 | |
|---|
| 106 | # IEnvironmentSetupParticipant methods |
|---|
| 107 | |
|---|
| 108 | def environment_created(self): |
|---|
| 109 | """Create the templates directory and some templates for |
|---|
| 110 | customization. |
|---|
| 111 | """ |
|---|
| 112 | def _create_file(filename, data=None): |
|---|
| 113 | fd = open(filename, 'w') |
|---|
| 114 | if data: |
|---|
| 115 | fd.write(data) |
|---|
| 116 | fd.close() |
|---|
| 117 | |
|---|
| 118 | if self.env.path: |
|---|
| 119 | templates_dir = os.path.join(self.env.path, 'templates') |
|---|
| 120 | if not os.path.exists(templates_dir): |
|---|
| 121 | os.mkdir(templates_dir) |
|---|
| 122 | _create_file(os.path.join(templates_dir, 'README'), |
|---|
| 123 | 'This directory contains project-specific custom ' |
|---|
| 124 | 'templates and style sheet.\n') |
|---|
| 125 | _create_file(os.path.join(templates_dir, 'site_header.cs'), |
|---|
| 126 | """<?cs |
|---|
| 127 | #################################################################### |
|---|
| 128 | # Site header - Contents are automatically inserted above Trac HTML |
|---|
| 129 | ?> |
|---|
| 130 | """) |
|---|
| 131 | _create_file(os.path.join(templates_dir, 'site_footer.cs'), |
|---|
| 132 | """<?cs |
|---|
| 133 | ######################################################################### |
|---|
| 134 | # Site footer - Contents are automatically inserted after main Trac HTML |
|---|
| 135 | ?> |
|---|
| 136 | """) |
|---|
| 137 | _create_file(os.path.join(templates_dir, 'site_css.cs'), |
|---|
| 138 | """<?cs |
|---|
| 139 | ################################################################## |
|---|
| 140 | # Site CSS - Place custom CSS, including overriding styles here. |
|---|
| 141 | ?> |
|---|
| 142 | """) |
|---|
| 143 | |
|---|
| 144 | def environment_needs_upgrade(self, db): |
|---|
| 145 | return False |
|---|
| 146 | |
|---|
| 147 | def upgrade_environment(self, db): |
|---|
| 148 | pass |
|---|
| 149 | |
|---|
| 150 | # IRequestHandler methods |
|---|
| 151 | |
|---|
| 152 | def match_request(self, req): |
|---|
| 153 | match = re.match(r'/chrome/(?P<prefix>[^/]+)/(?P<filename>[/\w\-\.]+)', |
|---|
| 154 | req.path_info) |
|---|
| 155 | if match: |
|---|
| 156 | req.args['prefix'] = match.group('prefix') |
|---|
| 157 | req.args['filename'] = match.group('filename') |
|---|
| 158 | return True |
|---|
| 159 | |
|---|
| 160 | def process_request(self, req): |
|---|
| 161 | prefix = req.args.get('prefix') |
|---|
| 162 | filename = req.args.get('filename') |
|---|
| 163 | |
|---|
| 164 | dirs = [] |
|---|
| 165 | for provider in self.template_providers: |
|---|
| 166 | for dir in [os.path.normpath(dir[1]) for dir |
|---|
| 167 | in provider.get_htdocs_dirs() if dir[0] == prefix]: |
|---|
| 168 | dirs.append(dir) |
|---|
| 169 | path = os.path.normpath(os.path.join(dir, filename)) |
|---|
| 170 | assert os.path.commonprefix([dir, path]) == dir |
|---|
| 171 | if os.path.isfile(path): |
|---|
| 172 | req.send_file(path) |
|---|
| 173 | |
|---|
| 174 | # FIXME: Should return a 404 error |
|---|
| 175 | self.log.warning('File %s not found in any of %s', filename, dirs) |
|---|
| 176 | raise TracError, 'File not found' |
|---|
| 177 | |
|---|
| 178 | # ITemplateProvider methods |
|---|
| 179 | |
|---|
| 180 | def get_htdocs_dirs(self): |
|---|
| 181 | from trac.config import default_dir |
|---|
| 182 | return [('common', default_dir('htdocs')), |
|---|
| 183 | ('site', self.env.get_htdocs_dir())] |
|---|
| 184 | |
|---|
| 185 | def get_templates_dirs(self): |
|---|
| 186 | return [self.env.get_templates_dir(), |
|---|
| 187 | self.config.get('trac', 'templates_dir')] |
|---|
| 188 | |
|---|
| 189 | # Public API methods |
|---|
| 190 | |
|---|
| 191 | def get_all_templates_dirs(self): |
|---|
| 192 | """Return a list of the names of all known templates directories.""" |
|---|
| 193 | dirs = [] |
|---|
| 194 | for provider in self.template_providers: |
|---|
| 195 | dirs += provider.get_templates_dirs() |
|---|
| 196 | return dirs |
|---|
| 197 | |
|---|
| 198 | def populate_hdf(self, req, handler): |
|---|
| 199 | """Add chrome-related data to the HDF.""" |
|---|
| 200 | |
|---|
| 201 | # Provided for template customization |
|---|
| 202 | req.hdf['HTTP.PathInfo'] = req.path_info |
|---|
| 203 | |
|---|
| 204 | href = Href(req.cgi_location) |
|---|
| 205 | req.hdf['chrome.href'] = href.chrome() |
|---|
| 206 | htdocs_location = self.config.get('trac', 'htdocs_location') or \ |
|---|
| 207 | href.chrome('common') |
|---|
| 208 | req.hdf['htdocs_location'] = htdocs_location.rstrip('/') + '/' |
|---|
| 209 | |
|---|
| 210 | # HTML <head> links |
|---|
| 211 | add_link(req, 'start', self.env.href.wiki()) |
|---|
| 212 | add_link(req, 'search', self.env.href.search()) |
|---|
| 213 | add_link(req, 'help', self.env.href.wiki('TracGuide')) |
|---|
| 214 | add_stylesheet(req, 'common/css/trac.css') |
|---|
| 215 | icon = self.config.get('project', 'icon') |
|---|
| 216 | if icon: |
|---|
| 217 | if not icon.startswith('/') and icon.find('://') == -1: |
|---|
| 218 | if '/' in icon: |
|---|
| 219 | icon = href.chrome(icon) |
|---|
| 220 | else: |
|---|
| 221 | icon = href.chrome('common', icon) |
|---|
| 222 | mimetype = mimeview.get_mimetype(icon) |
|---|
| 223 | add_link(req, 'icon', icon, mimetype=mimetype) |
|---|
| 224 | add_link(req, 'shortcut icon', icon, mimetype=mimetype) |
|---|
| 225 | |
|---|
| 226 | # Logo image |
|---|
| 227 | logo_link = self.config.get('header_logo', 'link') |
|---|
| 228 | logo_src = self.config.get('header_logo', 'src') |
|---|
| 229 | if logo_src: |
|---|
| 230 | logo_src_abs = logo_src.startswith('http://') or \ |
|---|
| 231 | logo_src.startswith('https://') |
|---|
| 232 | if not logo_src.startswith('/') and not logo_src_abs: |
|---|
| 233 | if '/' in logo_src: |
|---|
| 234 | logo_src = href.chrome(logo_src) |
|---|
| 235 | else: |
|---|
| 236 | logo_src = href.chrome('common', logo_src) |
|---|
| 237 | req.hdf['chrome.logo'] = { |
|---|
| 238 | 'link': logo_link, 'src': logo_src, 'src_abs': logo_src_abs, |
|---|
| 239 | 'alt': self.config.get('header_logo', 'alt'), |
|---|
| 240 | 'width': self.config.get('header_logo', 'width', ''), |
|---|
| 241 | 'height': self.config.get('header_logo', 'height', '') |
|---|
| 242 | } |
|---|
| 243 | else: |
|---|
| 244 | req.hdf['chrome.logo.link'] = logo_link |
|---|
| 245 | |
|---|
| 246 | # Navigation links |
|---|
| 247 | navigation = {} |
|---|
| 248 | active = None |
|---|
| 249 | for contributor in self.navigation_contributors: |
|---|
| 250 | for category, name, text in contributor.get_navigation_items(req): |
|---|
| 251 | navigation.setdefault(category, {})[name] = text |
|---|
| 252 | if contributor is handler: |
|---|
| 253 | active = contributor.get_active_navigation_item(req) |
|---|
| 254 | |
|---|
| 255 | for category, items in [(k, v.items()) for k, v in navigation.items()]: |
|---|
| 256 | order = [x.strip() for x |
|---|
| 257 | in self.config.get('trac', category).split(',')] |
|---|
| 258 | def navcmp(x, y): |
|---|
| 259 | if x[0] not in order: |
|---|
| 260 | return int(y[0] in order) |
|---|
| 261 | if y[0] not in order: |
|---|
| 262 | return -int(x[0] in order) |
|---|
| 263 | return cmp(order.index(x[0]), order.index(y[0])) |
|---|
| 264 | items.sort(navcmp) |
|---|
| 265 | |
|---|
| 266 | for name, text in items: |
|---|
| 267 | req.hdf['chrome.nav.%s.%s' % (category, name)] = text |
|---|
| 268 | if name == active: |
|---|
| 269 | req.hdf['chrome.nav.%s.%s.active' % (category, name)] = 1 |
|---|