1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """Content presentation for the web layer.
18
19 The Chrome module deals with delivering and shaping content to the end user,
20 mostly targeting (X)HTML generation but not exclusively, RSS or other forms of
21 web content are also using facilities provided here.
22 """
23
24 import datetime
25 from functools import partial
26 import itertools
27 import operator
28 import os.path
29 import pkg_resources
30 import pprint
31 import re
32 try:
33 from cStringIO import StringIO
34 except ImportError:
35 from StringIO import StringIO
36
37 from genshi import Markup
38 from genshi.builder import tag, Element
39 from genshi.core import Attrs, START
40 from genshi.filters import Translator
41 from genshi.output import DocType
42 from genshi.template import TemplateLoader, MarkupTemplate, NewTextTemplate
43
44 from trac.config import *
45 from trac.core import *
46 from trac.env import IEnvironmentSetupParticipant, ISystemInfoProvider
47 from trac.mimeview.api import RenderingContext, get_mimetype
48 from trac.perm import IPermissionRequestor
49 from trac.resource import *
50 from trac.util import as_bool, as_int, compat, get_reporter_id, html,\
51 presentation, get_pkginfo, pathjoin, translation
52 from trac.util.html import escape, plaintext
53 from trac.util.text import (
54 exception_to_unicode, is_obfuscated, javascript_quote,
55 obfuscate_email_address, pretty_size, shorten_line, to_js_string,
56 to_unicode, unicode_quote_plus)
57 from trac.util.datefmt import (
58 pretty_timedelta, datetime_now, format_datetime, format_date, format_time,
59 from_utimestamp, http_date, utc, get_date_format_jquery_ui, is_24_hours,
60 get_time_format_jquery_ui, user_time, get_month_names_jquery_ui,
61 get_day_names_jquery_ui, get_timezone_list_jquery_ui,
62 get_first_week_day_jquery_ui, get_timepicker_separator_jquery_ui,
63 get_period_names_jquery_ui, localtz)
64 from trac.util.html import to_fragment
65 from trac.util.translation import _, get_available_locales
66 from trac.web.api import IRequestHandler, ITemplateStreamFilter, HTTPNotFound
67 from trac.web.href import Href
68 from trac.wiki import IWikiSyntaxProvider
69 from trac.wiki.formatter import format_to, format_to_html, format_to_oneliner
70
71 default_mainnav_order = ('wiki', 'timeline', 'roadmap', 'browser',
72 'tickets', 'newticket', 'search', 'admin')
73 default_metanav_order = ('login', 'logout', 'prefs', 'help', 'about')
74
75
77 """Extension point interface for components that contribute items to the
78 navigation.
79 """
80
82 """This method is only called for the `IRequestHandler` processing the
83 request.
84
85 It should return the name of the navigation item that should be
86 highlighted as active/current.
87 """
88
90 """Should return an iterable object over the list of navigation items
91 to add, each being a tuple in the form (category, name, text).
92 """
93
94
96 """Extension point interface for components that provide their own
97 Genshi templates and accompanying static resources.
98 """
99
101 """Return a list of directories with static resources (such as style
102 sheets, images, etc.)
103
104 Each item in the list must be a `(prefix, abspath)` tuple. The
105 `prefix` part defines the path in the URL that requests to these
106 resources are prefixed with.
107
108 The `abspath` is the absolute path to the directory containing the
109 resources on the local file system.
110 """
111
113 """Return a list of directories containing the provided template
114 files.
115 """
116
117
123
124
125 -def add_link(req, rel, href, title=None, mimetype=None, classname=None,
126 **attrs):
127 """Add a link to the chrome info that will be inserted as <link> element in
128 the <head> of the generated HTML
129 """
130 linkid = '%s:%s' % (rel, href)
131 linkset = req.chrome.setdefault('linkset', set())
132 if linkid in linkset:
133 return
134
135 link = {'href': href, 'title': title, 'type': mimetype, 'class': classname}
136 link.update(attrs)
137 links = req.chrome.setdefault('links', {})
138 links.setdefault(rel, []).append(link)
139 linkset.add(linkid)
140
141
143 """Add a link to a style sheet to the chrome info so that it gets included
144 in the generated HTML page.
145
146 If `filename` is a network-path reference (i.e. starts with a protocol
147 or `//`), the return value will not be modified. If `filename` is absolute
148 (i.e. starts with `/`), the generated link will be based off the
149 application root path. If it is relative, the link will be based off the
150 `/chrome/` path.
151 """
152 href = chrome_resource_path(req, filename)
153 add_link(req, 'stylesheet', href, mimetype=mimetype, **attrs)
154
155
156 -def add_script(req, filename, mimetype='text/javascript', charset='utf-8',
157 ie_if=None):
158 """Add a reference to an external javascript file to the template.
159
160 If `filename` is a network-path reference (i.e. starts with a protocol
161 or `//`), the return value will not be modified. If `filename` is absolute
162 (i.e. starts with `/`), the generated link will be based off the
163 application root path. If it is relative, the link will be based off the
164 `/chrome/` path.
165 """
166 scriptset = req.chrome.setdefault('scriptset', set())
167 if filename in scriptset:
168 return False
169
170 href = chrome_resource_path(req, filename)
171 script = {'href': href, 'type': mimetype, 'charset': charset,
172 'prefix': Markup('<!--[if %s]>' % ie_if) if ie_if else None,
173 'suffix': Markup('<![endif]-->') if ie_if else None}
174
175 req.chrome.setdefault('scripts', []).append(script)
176 scriptset.add(filename)
177
178
180 """Add data to be made available in javascript scripts as global variables.
181
182 The keys in `data` and the keyword argument names provide the names of the
183 global variables. The values are converted to JSON and assigned to the
184 corresponding variables.
185 """
186 script_data = req.chrome.setdefault('script_data', {})
187 script_data.update(data)
188 script_data.update(kwargs)
189
190
192 """Add a non-fatal warning to the request object.
193
194 When rendering pages, all warnings will be rendered to the user. Note that
195 the message is escaped (and therefore converted to `Markup`) before it is
196 stored in the request object.
197 """
198 _add_message(req, 'warnings', msg, args)
199
200
202 """Add an informational notice to the request object.
203
204 When rendering pages, all notices will be rendered to the user. Note that
205 the message is escaped (and therefore converted to `Markup`) before it is
206 stored in the request object.
207 """
208 _add_message(req, 'notices', msg, args)
209
210
218
219
220 -def add_ctxtnav(req, elm_or_label, href=None, title=None):
221 """Add an entry to the current page's ctxtnav bar."""
222 if href:
223 elm = tag.a(elm_or_label, href=href, title=title)
224 else:
225 elm = elm_or_label
226 req.chrome.setdefault('ctxtnav', []).append(elm)
227
228
229 -def prevnext_nav(req, prev_label, next_label, up_label=None):
230 """Add Previous/Up/Next navigation links.
231
232 :param req: a `Request` object
233 :param prev_label: the label to use for left (previous) link
234 :param up_label: the label to use for the middle (up) link
235 :param next_label: the label to use for right (next) link
236 """
237 links = req.chrome['links']
238 prev_link = next_link = None
239
240 if not any(lnk in links for lnk in ('prev', 'up', 'next')):
241 return
242
243 if 'prev' in links:
244 prev = links['prev'][0]
245 prev_link = tag.a(prev_label, href=prev['href'], title=prev['title'],
246 class_='prev')
247
248 add_ctxtnav(req, tag.span(Markup('← '), prev_link or prev_label,
249 class_='missing' if not prev_link else None))
250
251 if up_label and 'up' in links:
252 up = links['up'][0]
253 add_ctxtnav(req, tag.a(up_label, href=up['href'], title=up['title']))
254
255 if 'next' in links:
256 next_ = links['next'][0]
257 next_link = tag.a(next_label, href=next_['href'], title=next_['title'],
258 class_='next')
259
260 add_ctxtnav(req, tag.span(next_link or next_label, Markup(' →'),
261 class_='missing' if not next_link else None))
262
263
264 -def web_context(req, resource=None, id=False, version=False, parent=False,
265 absurls=False):
266 """Create a rendering context from a request.
267
268 The `perm` and `href` properties of the context will be initialized
269 from the corresponding properties of the request object.
270
271 >>> from trac.test import Mock, MockPerm
272 >>> req = Mock(href=Mock(), perm=MockPerm())
273 >>> context = web_context(req)
274 >>> context.href is req.href
275 True
276 >>> context.perm is req.perm
277 True
278
279 :param req: the HTTP request object
280 :param resource: the `Resource` object or realm
281 :param id: the resource identifier
282 :param version: the resource version
283 :param absurls: whether URLs generated by the ``href`` object should
284 be absolute (including the protocol scheme and host
285 name)
286 :return: a new rendering context
287 :rtype: `RenderingContext`
288
289 :since: version 1.0
290 """
291 if req:
292 href = req.abs_href if absurls else req.href
293 perm = req.perm
294 else:
295 href = None
296 perm = None
297 self = RenderingContext(Resource(resource, id=id, version=version,
298 parent=parent), href=href, perm=perm)
299 self.req = req
300 return self
301
302
304 """Return an "authenticated" link to `link` for authenticated users.
305
306 If the user is anonymous, returns `link` unchanged. For authenticated
307 users, returns a link to `/login` that redirects to `link` after
308 authentication.
309 """
310 if req.authname != 'anonymous':
311 return req.href.login(referer=link)
312 return link
313
314
316 """Get script elements from chrome info of the request object during
317 rendering template or after rendering.
318
319 :param req: the HTTP request object.
320 :param use_late: if True, `late_links` will be used instead of `links`.
321 """
322 chrome = req.chrome
323 if use_late:
324 links = chrome.get('late_links', {}).get('stylesheet', [])
325 scripts = chrome.get('late_scripts', [])
326 script_data = chrome.get('late_script_data', {})
327 else:
328 links = chrome.get('early_links', {}).get('stylesheet', []) + \
329 chrome.get('links', {}).get('stylesheet', [])
330 scripts = chrome.get('early_scripts', []) + chrome.get('scripts', [])
331 script_data = {}
332 script_data.update(chrome.get('early_script_data', {}))
333 script_data.update(chrome.get('script_data', {}))
334
335 content = []
336 content.extend('jQuery.loadStyleSheet(%s, %s);' %
337 (to_js_string(link['href']), to_js_string(link['type']))
338 for link in links or ())
339 content.extend('var %s=%s;' % (name, presentation.to_json(value))
340 for name, value in (script_data or {}).iteritems())
341
342 fragment = tag()
343 if content:
344 fragment.append(tag.script('\n'.join(content), type='text/javascript'))
345 for script in scripts:
346 fragment.append(script['prefix'])
347 fragment.append(tag.script(
348 'jQuery.loadScript(%s, %s, %s)' %
349 (to_js_string(script['href']), to_js_string(script['type']),
350 to_js_string(script['charset'])), type='text/javascript'))
351 fragment.append(script['suffix'])
352
353 return fragment
354
355
357 """Get the path for a chrome resource given its `filename`.
358
359 If `filename` is a network-path reference (i.e. starts with a protocol
360 or `//`), the return value will not be modified. If `filename` is absolute
361 (i.e. starts with `/`), the generated link will be based off the
362 application root path. If it is relative, the link will be based off the
363 `/chrome/` path.
364 """
365 if filename.startswith(('http://', 'https://', '//')):
366 return filename
367 elif filename.startswith('common/') and 'htdocs_location' in req.chrome:
368 return Href(req.chrome['htdocs_location'])(filename[7:])
369 else:
370 href = req.href if filename.startswith('/') else req.href.chrome
371 return href(filename)
372
373
374 _chrome_resource_path = chrome_resource_path
375
376
378 """Save warnings and notices in case of redirect, so that they can
379 be displayed after the redirect."""
380 for type_ in ['warnings', 'notices']:
381 for (i, message) in enumerate(req.chrome[type_]):
382 req.session['chrome.%s.%d' % (type_, i)] = escape(message, False)
383
384
385
386 _translate_nop = "".join([chr(i) for i in range(256)])
387 _invalid_control_chars = "".join([chr(i) for i in range(32)
388 if i not in [0x09, 0x0a, 0x0d]])
389
390
392 """Web site chrome assembly manager.
393
394 Chrome is everything that is not actual page content.
395 """
396
397 implements(ISystemInfoProvider, IEnvironmentSetupParticipant,
398 IPermissionRequestor, IRequestHandler, ITemplateProvider,
399 IWikiSyntaxProvider)
400
401 required = True
402 is_valid_default_handler = False
403
404 navigation_contributors = ExtensionPoint(INavigationContributor)
405 template_providers = ExtensionPoint(ITemplateProvider)
406 stream_filters = ExtensionPoint(ITemplateStreamFilter)
407
408 shared_templates_dir = PathOption('inherit', 'templates_dir', '',
409 """Path to the //shared templates directory//.
410
411 Templates in that directory are loaded in addition to those in the
412 environments `templates` directory, but the latter take precedence.
413
414 Non-absolute paths are relative to the Environment `conf`
415 directory.
416 """)
417
418 shared_htdocs_dir = PathOption('inherit', 'htdocs_dir', '',
419 """Path to the //shared htdocs directory//.
420
421 Static resources in that directory are mapped to /chrome/shared
422 under the environment URL, in addition to common and site locations.
423
424 This can be useful in site.html for common interface customization
425 of multiple Trac environments.
426
427 Non-absolute paths are relative to the Environment `conf`
428 directory.
429 (''since 1.0'')""")
430
431 auto_reload = BoolOption('trac', 'auto_reload', False,
432 """Automatically reload template files after modification.""")
433
434 genshi_cache_size = IntOption('trac', 'genshi_cache_size', 128,
435 """The maximum number of templates that the template loader will cache
436 in memory. You may want to choose a higher value if your site uses a
437 larger number of templates, and you have enough memory to spare, or
438 you can reduce it if you are short on memory.""")
439
440 htdocs_location = Option('trac', 'htdocs_location', '',
441 """Base URL for serving the core static resources below
442 `/chrome/common/`.
443
444 It can be left empty, and Trac will simply serve those resources
445 itself.
446
447 Advanced users can use this together with
448 [TracAdmin trac-admin ... deploy <deploydir>] to allow serving the
449 static resources for Trac directly from the web server.
450 Note however that this only applies to the `<deploydir>/htdocs/common`
451 directory, the other deployed resources (i.e. those from plugins)
452 will not be made available this way and additional rewrite
453 rules will be needed in the web server.""")
454
455 jquery_location = Option('trac', 'jquery_location', '',
456 """Location of the jQuery !JavaScript library (version %(version)s).
457
458 An empty value loads jQuery from the copy bundled with Trac.
459
460 Alternatively, jQuery could be loaded from a CDN, for example:
461 http://code.jquery.com/jquery-%(version)s.min.js,
462 http://ajax.aspnetcdn.com/ajax/jQuery/jquery-%(version)s.min.js or
463 https://ajax.googleapis.com/ajax/libs/jquery/%(version)s/jquery.min.js.
464
465 (''since 1.0'')""", doc_args={'version': '1.11.3'})
466
467 jquery_ui_location = Option('trac', 'jquery_ui_location', '',
468 """Location of the jQuery UI !JavaScript library (version %(version)s).
469
470 An empty value loads jQuery UI from the copy bundled with Trac.
471
472 Alternatively, jQuery UI could be loaded from a CDN, for example:
473 https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/jquery-ui.min.js
474 or
475 http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/jquery-ui.min.js.
476
477 (''since 1.0'')""", doc_args={'version': '1.11.4'})
478
479 jquery_ui_theme_location = Option('trac', 'jquery_ui_theme_location', '',
480 """Location of the theme to be used with the jQuery UI !JavaScript
481 library (version %(version)s).
482
483 An empty value loads the custom Trac jQuery UI theme from the copy
484 bundled with Trac.
485
486 Alternatively, a jQuery UI theme could be loaded from a CDN, for
487 example:
488 https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/themes/start/jquery-ui.css
489 or
490 http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/themes/start/jquery-ui.css.
491
492 (''since 1.0'')""", doc_args={'version': '1.11.4'})
493
494 mainnav = ConfigSection('mainnav', """Configures the main navigation bar,
495 which by default contains //Wiki//, //Timeline//, //Roadmap//,
496 //Browse Source//, //View Tickets//, //New Ticket//, //Search// and
497 //Admin//.
498
499 The `label`, `href`, and `order` attributes can be specified. Entries
500 can be disabled by setting the value of the navigation item to
501 `disabled`.
502
503 The following example renames the link to WikiStart to //Home//,
504 links the //View Tickets// entry to a specific report and disables
505 the //Search// entry.
506 {{{#!ini
507 [mainnav]
508 wiki.label = Home
509 tickets.href = /report/24
510 search = disabled
511 }}}
512
513 See TracNavigation for more details.
514 """)
515
516 metanav = ConfigSection('metanav', """Configures the meta navigation
517 entries, which by default are //Login//, //Logout//, //Preferences//,
518 ''!Help/Guide'' and //About Trac//. The allowed attributes are the
519 same as for `[mainnav]`. Additionally, a special entry is supported -
520 `logout.redirect` is the page the user sees after hitting the logout
521 button. For example:
522
523 {{{#!ini
524 [metanav]
525 logout.redirect = wiki/Logout
526 }}}
527
528 See TracNavigation for more details.
529 """)
530
531 logo_link = Option('header_logo', 'link', '',
532 """URL to link to, from the header logo.""")
533
534 logo_src = Option('header_logo', 'src', 'site/your_project_logo.png',
535 """URL of the image to use as header logo.
536 It can be absolute, server relative or relative.
537
538 If relative, it is relative to one of the `/chrome` locations:
539 `site/your-logo.png` if `your-logo.png` is located in the `htdocs`
540 folder within your TracEnvironment;
541 `common/your-logo.png` if `your-logo.png` is located in the
542 folder mapped to the [#trac-section htdocs_location] URL.
543 Only specifying `your-logo.png` is equivalent to the latter.""")
544
545 logo_alt = Option('header_logo', 'alt',
546 "(please configure the [header_logo] section in trac.ini)",
547 """Alternative text for the header logo.""")
548
549 logo_width = IntOption('header_logo', 'width', -1,
550 """Width of the header logo image in pixels.""")
551
552 logo_height = IntOption('header_logo', 'height', -1,
553 """Height of the header logo image in pixels.""")
554
555 show_email_addresses = BoolOption('trac', 'show_email_addresses', 'false',
556 """Show email addresses instead of usernames. If false, email
557 addresses are obfuscated for users that don't have EMAIL_VIEW
558 permission.
559 """)
560
561 show_full_names = BoolOption('trac', 'show_full_names', 'true',
562 """Show full names instead of usernames. (//since 1.2//)""")
563
564 never_obfuscate_mailto = BoolOption('trac', 'never_obfuscate_mailto',
565 'false',
566 """Never obfuscate `mailto:` links explicitly written in the wiki,
567 even if `show_email_addresses` is false or the user doesn't have
568 EMAIL_VIEW permission.
569 """)
570
571 show_ip_addresses = BoolOption('trac', 'show_ip_addresses', 'false',
572 """Show IP addresses for resource edits (e.g. wiki). Since 1.0.5 this
573 option is deprecated and will be removed in 1.3.1.
574 """)
575
576 resizable_textareas = BoolOption('trac', 'resizable_textareas', 'true',
577 """Make `<textarea>` fields resizable. Requires !JavaScript.
578 (''since 0.12'')""")
579
580 wiki_toolbars = BoolOption('trac', 'wiki_toolbars', 'true',
581 """Add a simple toolbar on top of Wiki <textarea>s.
582 (''since 1.0.2'')""")
583
584 auto_preview_timeout = FloatOption('trac', 'auto_preview_timeout', 2.0,
585 """Inactivity timeout in seconds after which the automatic wiki preview
586 triggers an update. This option can contain floating-point values. The
587 lower the setting, the more requests will be made to the server. Set
588 this to 0 to disable automatic preview. (''since 0.12'')""")
589
590 default_dateinfo_format = ChoiceOption('trac', 'default_dateinfo_format',
591 ('relative', 'absolute'),
592 """The date information format. Valid options are 'relative' for
593 displaying relative format and 'absolute' for displaying absolute
594 format. (''since 1.0'')""")
595
596 use_chunked_encoding = BoolOption('trac', 'use_chunked_encoding', 'false',
597 """If enabled, send contents as chunked encoding in HTTP/1.1.
598 Otherwise, send contents with `Content-Length` header after entire of
599 the contents are rendered. (''since 1.0.6'')""")
600
601 templates = None
602
603
604 html_doctype = DocType.XHTML_STRICT
605
606
607 _default_context_data = {
608 '_': translation.gettext,
609 'all': all,
610 'any': any,
611 'as_bool': as_bool,
612 'as_int': as_int,
613 'classes': presentation.classes,
614 'date': datetime.date,
615 'datetime': datetime.datetime,
616 'dgettext': translation.dgettext,
617 'dngettext': translation.dngettext,
618 'first_last': presentation.first_last,
619 'find_element': html.find_element,
620 'get_reporter_id': get_reporter_id,
621 'gettext': translation.gettext,
622 'group': presentation.group,
623 'groupby': compat.py_groupby,
624 'http_date': http_date,
625 'is_obfuscated': is_obfuscated,
626 'istext': presentation.istext,
627 'javascript_quote': javascript_quote,
628 'ngettext': translation.ngettext,
629 'operator': operator,
630 'paginate': presentation.paginate,
631 'partial': partial,
632 'pathjoin': pathjoin,
633 'plaintext': plaintext,
634 'pprint': pprint.pformat,
635 'pretty_size': pretty_size,
636 'pretty_timedelta': pretty_timedelta,
637 'quote_plus': unicode_quote_plus,
638 'reversed': reversed,
639 'separated': presentation.separated,
640 'shorten_line': shorten_line,
641 'sorted': sorted,
642 'time': datetime.time,
643 'timedelta': datetime.timedelta,
644 'to_json': presentation.to_json,
645 'to_unicode': to_unicode,
646 'utc': utc,
647 }
648
649
650
652 import genshi
653 info = get_pkginfo(genshi).get('version')
654 if hasattr(genshi, '_speedups'):
655 info += ' (with speedups)'
656 else:
657 info += ' (without speedups)'
658 yield 'Genshi', info
659 try:
660 import babel
661 except ImportError:
662 babel = None
663 if babel is not None:
664 info = get_pkginfo(babel).get('version')
665 if not get_available_locales():
666 info += " (translations unavailable)"
667 self.log.warning("Locale data is missing")
668 yield 'Babel', info
669
670
671
673 """Create the environment templates directory."""
674 if self.env.path:
675 templates_dir = os.path.join(self.env.path, 'templates')
676 if not os.path.exists(templates_dir):
677 os.mkdir(templates_dir)
678
679 site_path = os.path.join(templates_dir, 'site.html.sample')
680 with open(site_path, 'w') as fileobj:
681 fileobj.write("""\
682 <html xmlns="http://www.w3.org/1999/xhtml"
683 xmlns:xi="http://www.w3.org/2001/XInclude"
684 xmlns:py="http://genshi.edgewall.org/"
685 py:strip="">
686 <!--!
687 This file allows customizing the appearance of the Trac installation.
688 Add your customizations here and rename the file to site.html. Note that
689 it will take precedence over a global site.html placed in the directory
690 specified by [inherit] templates_dir.
691
692 More information about site appearance customization can be found here:
693
694 https://trac.edgewall.org/wiki/TracInterfaceCustomization#SiteAppearance
695 -->
696 </html>
697 """)
698
701
704
705
706
708 match = re.match(r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>.+)',
709 req.path_info)
710 if match:
711 req.args['prefix'] = match.group('prefix')
712 req.args['filename'] = match.group('filename')
713 return True
714
734
735
736
738 """`EMAIL_VIEW` permission allows for showing email addresses even
739 if `[trac] show_email_addresses` is `false`."""
740 return ['EMAIL_VIEW']
741
742
743
748
755
756
757
760
762 yield ('htdocs', self._format_link)
763
768
769
770
777
779 """Prepare the basic chrome data for the request.
780
781 :param req: the request object
782 :param handler: the `IRequestHandler` instance that is processing the
783 request
784 """
785 self.log.debug('Prepare chrome data for request')
786
787 chrome = {'metas': [], 'links': {}, 'scripts': [], 'script_data': {},
788 'ctxtnav': [], 'warnings': [], 'notices': []}
789 req.chrome = chrome
790
791 htdocs_location = self.htdocs_location or req.href.chrome('common')
792 chrome['htdocs_location'] = htdocs_location.rstrip('/') + '/'
793
794
795 add_link(req, 'start', req.href.wiki())
796 add_link(req, 'search', req.href.search())
797 add_link(req, 'help', req.href.wiki('TracGuide'))
798 add_stylesheet(req, 'common/css/trac.css')
799 add_script(req, self.jquery_location or 'common/js/jquery.js')
800
801 if handler is not None and \
802 getattr(handler.__class__, 'jquery_noconflict', False):
803 add_script(req, 'common/js/noconflict.js')
804 add_script(req, 'common/js/babel.js')
805 if req.locale is not None and str(req.locale) != 'en_US':
806 add_script(req, 'common/js/messages/%s.js' % req.locale)
807 add_script(req, 'common/js/trac.js')
808 add_script(req, 'common/js/search.js')
809
810
811 chrome['icon'] = self.get_icon_data(req)
812 if chrome['icon']:
813 src = chrome['icon']['src']
814 mimetype = chrome['icon']['mimetype']
815 add_link(req, 'icon', src, mimetype=mimetype)
816
817
818 chrome['logo'] = self.get_logo_data(req.href, req.abs_href)
819
820
821 allitems = {}
822 active = None
823 for contributor in self.navigation_contributors:
824 try:
825 for category, name, text in \
826 contributor.get_navigation_items(req) or []:
827 category_section = self.config[category]
828 if category_section.getbool(name, True):
829
830 item = text if isinstance(text, Element) and \
831 text.tag.localname == 'a' \
832 else None
833 label = category_section.get(name + '.label')
834 href = category_section.get(name + '.href')
835 if href and href.startswith('/'):
836 href = req.href + href
837 if item:
838 if label:
839 item.children[0] = label
840 if href:
841 item = item(href=href)
842 else:
843 if href or label:
844 item = tag.a(label or text, href=href)
845 else:
846 item = text
847 allitems.setdefault(category, {})[name] = item
848 if contributor is handler:
849 active = contributor.get_active_navigation_item(req)
850 except Exception as e:
851 name = contributor.__class__.__name__
852 if isinstance(e, TracError):
853 self.log.warning("Error with navigation contributor %s: "
854 "%s", name, exception_to_unicode(e))
855 else:
856 self.log.error("Error with navigation contributor %s: %s",
857 name,
858 exception_to_unicode(e, traceback=True))
859 add_warning(req, _("Error with navigation contributor "
860 '"%(name)s"', name=name))
861
862 nav = {}
863 for category, navitems in allitems.items():
864 sect = self.config[category]
865 order = dict((name, sect.getfloat(name + '.order', float('inf')))
866 for name in navitems)
867 nav[category] = []
868 for name, label in navitems.items():
869 nav[category].append({
870 'name': name,
871 'label': label,
872 'active': name == active
873 })
874 nav[category].sort(key=lambda e: (order[e['name']], e['name']))
875
876 chrome['nav'] = nav
877
878
879 chrome['theme'] = 'theme.html'
880
881
882 req.add_redirect_listener(_save_messages)
883
884 return chrome
885
887 icon = {}
888 icon_src = icon_abs_src = self.env.project_icon
889 if icon_src:
890 if not icon_src.startswith('/') and icon_src.find('://') == -1:
891 if '/' in icon_src:
892 icon_abs_src = req.abs_href.chrome(icon_src)
893 icon_src = req.href.chrome(icon_src)
894 else:
895 icon_abs_src = req.abs_href.chrome('common', icon_src)
896 icon_src = req.href.chrome('common', icon_src)
897 mimetype = get_mimetype(icon_src)
898 icon = {'src': icon_src, 'abs_src': icon_abs_src,
899 'mimetype': mimetype}
900 return icon
901
903
904 logo = {}
905 logo_src = self.logo_src
906 if logo_src:
907 abs_href = abs_href or href
908 if logo_src.startswith(('http://', 'https://', '/')):
909
910 logo_src_abs = logo_src
911 elif '/' in logo_src:
912
913 logo_src_abs = abs_href.chrome(logo_src)
914 logo_src = href.chrome(logo_src)
915 else:
916
917 logo_src_abs = abs_href.chrome('common', logo_src)
918 logo_src = href.chrome('common', logo_src)
919 width = self.logo_width if self.logo_width > -1 else None
920 height = self.logo_height if self.logo_height > -1 else None
921 logo = {
922 'link': self.logo_link, 'src': logo_src,
923 'src_abs': logo_src_abs, 'alt': self.logo_alt,
924 'width': width, 'height': height
925 }
926 else:
927 logo = {'link': self.logo_link, 'alt': self.logo_alt}
928 return logo
929
931 d = self._default_context_data.copy()
932 d['trac'] = {
933 'version': self.env.trac_version,
934 'homepage': 'https://trac.edgewall.org/',
935 }
936
937 href = req and req.href
938 abs_href = req.abs_href if req else self.env.abs_href
939 admin_href = None
940 if self.env.project_admin_trac_url == '.':
941 admin_href = href
942 elif self.env.project_admin_trac_url:
943 admin_href = Href(self.env.project_admin_trac_url)
944
945 d['project'] = {
946 'name': self.env.project_name,
947 'descr': self.env.project_description,
948 'url': self.env.project_url,
949 'admin': self.env.project_admin,
950 'admin_href': admin_href,
951 'admin_trac_url': self.env.project_admin_trac_url,
952 }
953 footer = self.env.project_footer
954 d['chrome'] = {
955 'footer': Markup(footer and translation.gettext(footer))
956 }
957 if req:
958 d['chrome'].update(req.chrome)
959 else:
960 d['chrome'].update({
961 'htdocs_location': self.htdocs_location,
962 'logo': self.get_logo_data(self.env.abs_href),
963 })
964
965 try:
966 show_email_addresses = self.show_email_addresses or \
967 not req or 'EMAIL_VIEW' in req.perm
968 except Exception as e:
969
970
971 self.log.error("Error during check of EMAIL_VIEW: %s",
972 exception_to_unicode(e))
973 show_email_addresses = False
974
975 def pretty_dateinfo(date, format=None, dateonly=False):
976 if not date:
977 return ''
978 if format == 'date':
979 absolute = user_time(req, format_date, date)
980 else:
981 absolute = user_time(req, format_datetime, date)
982 now = datetime_now(localtz)
983 relative = pretty_timedelta(date, now)
984 if not format:
985 format = req.session.get('dateinfo',
986 self.default_dateinfo_format)
987 in_or_ago = _("in %(relative)s", relative=relative) \
988 if date > now else \
989 _("%(relative)s ago", relative=relative)
990 if format == 'relative':
991 label = in_or_ago if not dateonly else relative
992 title = absolute
993 else:
994 if dateonly:
995 label = absolute
996 elif req.lc_time == 'iso8601':
997 label = _("at %(iso8601)s", iso8601=absolute)
998 else:
999 label = _("on %(date)s at %(time)s",
1000 date=user_time(req, format_date, date),
1001 time=user_time(req, format_time, date))
1002 title = in_or_ago
1003 return tag.span(label, title=title)
1004
1005 def dateinfo(date):
1006 return pretty_dateinfo(date, format='relative', dateonly=True)
1007
1008 def get_rel_url(resource, **kwargs):
1009 return get_resource_url(self.env, resource, href, **kwargs)
1010
1011 def get_abs_url(resource, **kwargs):
1012 return get_resource_url(self.env, resource, abs_href, **kwargs)
1013
1014 dateinfo_format = \
1015 req.session.get('dateinfo', self.default_dateinfo_format) \
1016 if req else self.default_dateinfo_format
1017
1018 d.update({
1019 'context': web_context(req) if req else None,
1020 'Resource': Resource,
1021 'url_of': get_rel_url,
1022 'abs_url_of': get_abs_url,
1023 'name_of': partial(get_resource_name, self.env),
1024 'shortname_of': partial(get_resource_shortname, self.env),
1025 'summary_of': partial(get_resource_summary, self.env),
1026 'resource_link': partial(render_resource_link, self.env),
1027 'req': req,
1028 'abs_href': abs_href,
1029 'href': href,
1030 'perm': req and req.perm,
1031 'authname': req.authname if req else '<trac>',
1032 'locale': req and req.locale,
1033
1034 'show_email_addresses': show_email_addresses,
1035 'show_ip_addresses': self.show_ip_addresses,
1036 'author_email': partial(self.author_email,
1037 email_map=self.get_email_map()),
1038 'authorinfo': partial(self.authorinfo, req),
1039 'authorinfo_short': self.authorinfo_short,
1040 'format_author': partial(self.format_author, req),
1041 'format_emails': self.format_emails,
1042 'get_systeminfo': self.env.get_systeminfo,
1043 'captioned_button': partial(presentation.captioned_button, req),
1044
1045
1046 'dateinfo': dateinfo,
1047 'dateinfo_format': dateinfo_format,
1048 'pretty_dateinfo': pretty_dateinfo,
1049 'format_datetime': partial(user_time, req, format_datetime),
1050 'format_date': partial(user_time, req, format_date),
1051 'format_time': partial(user_time, req, format_time),
1052 'fromtimestamp': partial(datetime.datetime.fromtimestamp,
1053 tz=req and req.tz),
1054 'from_utimestamp': from_utimestamp,
1055
1056
1057 'wiki_to': partial(format_to, self.env),
1058 'wiki_to_html': partial(format_to_html, self.env),
1059 'wiki_to_oneliner': partial(format_to_oneliner, self.env),
1060 })
1061
1062
1063 d.update(data)
1064 return d
1065
1087
1088 - def render_template(self, req, filename, data, content_type=None,
1089 fragment=False, iterable=False, method=None):
1090 """Render the `filename` using the `data` for the context.
1091
1092 The `content_type` argument is used to choose the kind of template
1093 used (`NewTextTemplate` if `'text/plain'`, `MarkupTemplate`
1094 otherwise), and tweak the rendering process. Doctype for `'text/html'`
1095 can be specified by setting the `html_doctype` attribute (default
1096 is `XHTML_STRICT`)
1097
1098 The rendering `method` (xml, xhtml or text) may be specified and is
1099 inferred from the `content_type` if not specified.
1100
1101 When `fragment` is specified, the (filtered) Genshi stream is
1102 returned.
1103
1104 When `iterable` is specified, the content as an iterable instance
1105 which is generated from filtered Genshi stream is returned.
1106 """
1107 if content_type is None:
1108 content_type = 'text/html'
1109
1110 if method is None:
1111 method = {'text/html': 'xhtml',
1112 'text/plain': 'text'}.get(content_type, 'xml')
1113
1114 if method == "xhtml":
1115
1116 for type_ in ['warnings', 'notices']:
1117 try:
1118 for i in itertools.count():
1119 message = Markup(req.session.pop('chrome.%s.%d'
1120 % (type_, i)))
1121 if message not in req.chrome[type_]:
1122 req.chrome[type_].append(message)
1123 except KeyError:
1124 pass
1125
1126 template = self.load_template(filename, method=method)
1127 data = self.populate_data(req, data)
1128 data['chrome']['content_type'] = content_type
1129
1130 stream = template.generate(**data)
1131
1132
1133 if self.stream_filters:
1134 stream |= self._filter_stream(req, method, filename, stream, data)
1135
1136 if fragment:
1137 return stream
1138
1139 if method == 'text':
1140 buffer = StringIO()
1141 stream.render('text', out=buffer, encoding='utf-8')
1142 return buffer.getvalue()
1143
1144 doctype = None
1145 if content_type == 'text/html':
1146 doctype = self.html_doctype
1147 if req.form_token:
1148 stream |= self._add_form_token(req.form_token)
1149 if not req.session.as_int('accesskeys', 0):
1150 stream |= self._strip_accesskeys
1151
1152 links = req.chrome.get('links')
1153 scripts = req.chrome.get('scripts')
1154 script_data = req.chrome.get('script_data')
1155 req.chrome.update({'early_links': links, 'early_scripts': scripts,
1156 'early_script_data': script_data,
1157 'links': {}, 'scripts': [], 'script_data': {}})
1158 data.setdefault('chrome', {}).update({
1159 'late_links': req.chrome['links'],
1160 'late_scripts': req.chrome['scripts'],
1161 'late_script_data': req.chrome['script_data'],
1162 })
1163
1164 if iterable:
1165 return self.iterable_content(stream, method, doctype=doctype)
1166
1167 try:
1168 buffer = StringIO()
1169 stream.render(method, doctype=doctype, out=buffer,
1170 encoding='utf-8')
1171 return buffer.getvalue().translate(_translate_nop,
1172 _invalid_control_chars)
1173 except Exception as e:
1174
1175 req.chrome.update({'early_links': None, 'early_scripts': None,
1176 'early_script_data': None, 'links': links,
1177 'scripts': scripts, 'script_data': script_data})
1178
1179 if isinstance(e, UnicodeError):
1180 pos = self._stream_location(stream)
1181 if pos:
1182 location = "'%s', line %s, char %s" % pos
1183 else:
1184 location = '%s %s' % (filename,
1185 _("(unknown template location)"))
1186 raise TracError(_("Genshi %(error)s error while rendering "
1187 "template %(location)s",
1188 error=e.__class__.__name__,
1189 location=location))
1190 raise
1191
1193 """Returns a dictionary containing the lists of files present in the
1194 site and shared templates and htdocs directories.
1195 """
1196 def list_dir(path, suffix=None):
1197 if not os.path.isdir(path):
1198 return []
1199 return sorted(name for name in os.listdir(path)
1200 if suffix is None or name.endswith(suffix))
1201
1202 files = {}
1203
1204 site_templates = list_dir(self.env.templates_dir, '.html')
1205 shared_templates = list_dir(Chrome(self.env).shared_templates_dir,
1206 '.html')
1207
1208
1209 site_htdocs = list_dir(self.env.htdocs_dir)
1210 shared_htdocs = list_dir(Chrome(self.env).shared_htdocs_dir)
1211
1212 if any((site_templates, shared_templates, site_htdocs, shared_htdocs)):
1213 files = {
1214 'site-templates': site_templates,
1215 'shared-templates': shared_templates,
1216 'site-htdocs': site_htdocs,
1217 'shared-htdocs': shared_htdocs,
1218 }
1219 return files
1220
1221 - def iterable_content(self, stream, method, **kwargs):
1222 """Generate an iterable object which iterates `str` instances
1223 from the given stream instance.
1224
1225 :param method: the serialization method; can be either "xml",
1226 "xhtml", "html", "text", or a custom serializer
1227 class
1228 """
1229 try:
1230 if method == 'text':
1231 for chunk in stream.serialize(method, **kwargs):
1232 yield chunk.encode('utf-8')
1233 else:
1234 for chunk in stream.serialize(method, **kwargs):
1235 yield chunk.encode('utf-8') \
1236 .translate(_translate_nop,
1237 _invalid_control_chars)
1238 except Exception as e:
1239 pos = self._stream_location(stream)
1240 if pos:
1241 location = "'%s', line %s, char %s" % pos
1242 else:
1243 location = '(unknown template location)'
1244 self.log.error('Genshi %s error while rendering template %s%s',
1245 e.__class__.__name__, location,
1246 exception_to_unicode(e, traceback=True))
1247
1248
1249
1251 """Returns the author email from the `email_map` if `author`
1252 doesn't look like an email address."""
1253 if email_map and '@' not in author and email_map.get(author):
1254 author = email_map.get(author)
1255 return author
1256
1257 - def authorinfo(self, req, author, email_map=None, resource=None):
1258 """Format a username to HTML.
1259
1260 Calls `Chrome.format_author` to format the username, and wraps
1261 the formatted username in a `span` with class `trac-author`,
1262 `trac-author-anonymous` or `trac-author-none`.
1263
1264 :param req: the `Request` object.
1265 :param author: the author string to be formatted.
1266 :param email_map: dictionary mapping usernames to email addresses.
1267 :param resource: optional `Resource` object for `EMAIL_VIEW`
1268 fine-grained permissions checks.
1269
1270 :since 1.1.6: accepts the optional `resource` keyword parameter.
1271 """
1272 author = self.author_email(author, email_map)
1273 return tag.span(self.format_author(req, author, resource),
1274 class_=self.author_class(req, author))
1275
1277 suffix = ''
1278 if author == 'anonymous':
1279 suffix = '-anonymous'
1280 elif not author:
1281 suffix = '-none'
1282 elif req and author == req.authname:
1283 suffix = '-user'
1284 return 'trac-author' + suffix
1285
1286 _long_author_re = re.compile(r'.*<([^@]+)@[^@]+>\s*|([^@]+)@[^@]+')
1287
1289 shortened = None
1290 match = self._long_author_re.match(author or '')
1291 if match:
1292 shortened = match.group(1) or match.group(2)
1293 return self.authorinfo(None, shortened or author)
1294
1296 """Split a CC: value in a list of addresses."""
1297 ccs = []
1298 for cc in re.split(r'[;,]', cc_field or ''):
1299 cc = cc.strip()
1300 if cc:
1301 ccs.append(cc)
1302 return ccs
1303
1343
1355
1364
1365
1366
1367 - def add_textarea_grips(self, req):
1368 """Make `<textarea class="trac-resizable">` fields resizable if enabled
1369 by configuration."""
1370 if self.resizable_textareas:
1371 add_script(req, 'common/js/resizer.js')
1372
1378
1384
1386 """Add a reference to the jQuery UI script and link the stylesheet."""
1387 add_script(req, self.jquery_ui_location
1388 or 'common/js/jquery-ui.js')
1389 add_stylesheet(req, self.jquery_ui_theme_location
1390 or 'common/css/jquery-ui/jquery-ui.css')
1391 add_script(req, 'common/js/jquery-ui-addons.js')
1392 add_stylesheet(req, 'common/css/jquery-ui-addons.css')
1393 is_iso8601 = req.lc_time == 'iso8601'
1394 now = datetime_now(req.tz)
1395 tzoffset = now.strftime('%z')
1396 if is_iso8601:
1397 default_timezone = (-1 if tzoffset.startswith('-') else 1) * \
1398 (int(tzoffset[1:3]) * 60 + int(tzoffset[3:5]))
1399 timezone_list = get_timezone_list_jquery_ui(now)
1400 else:
1401 default_timezone = None
1402 timezone_list = None
1403 add_script_data(req, jquery_ui={
1404 'month_names': get_month_names_jquery_ui(req),
1405 'day_names': get_day_names_jquery_ui(req),
1406 'date_format': get_date_format_jquery_ui(req.lc_time),
1407 'time_format': get_time_format_jquery_ui(req.lc_time),
1408 'ampm': not is_24_hours(req.lc_time),
1409 'period_names': get_period_names_jquery_ui(req),
1410 'first_week_day': get_first_week_day_jquery_ui(req),
1411 'timepicker_separator': get_timepicker_separator_jquery_ui(req),
1412 'show_timezone': is_iso8601,
1413 'default_timezone': default_timezone,
1414 'timezone_list': timezone_list,
1415 'timezone_iso8601': is_iso8601,
1416 })
1417 add_script(req, 'common/js/jquery-ui-i18n.js')
1418
1419
1420
1434 return _generate
1435
1437 for kind, data, pos in stream:
1438 if kind is START and 'accesskey' in data[1]:
1439 data = data[0], Attrs([(k, v) for k, v in data[1]
1440 if k != 'accesskey'])
1441 yield kind, data, pos
1442
1449 return inner
1450
1452 for kind, data, pos in stream:
1453 return pos
1454