1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2004-2021 Edgewall Software
4# Copyright (C) 2004-2005 Daniel Lundin <daniel@edgewall.com>
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 https://trac.edgewall.org/wiki/TracLicense.
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 https://trac.edgewall.org/log/.
14#
15# Author: Daniel Lundin <daniel@edgewall.com>
16
17import math
18import pkg_resources
19import re
20
21from trac.core import *
22from trac.prefs.api import IPreferencePanelProvider
23from trac.util import as_float, lazy
24from trac.util.datefmt import all_timezones, get_timezone, localtz
25from trac.util.html import tag
26from trac.util.translation import _, Locale, deactivate,\
27                                  get_available_locales, get_locale_name, \
28                                  make_activable
29from trac.web.api import HTTPNotFound, IRequestHandler, \
30                         is_valid_default_handler
31from trac.web.chrome import Chrome, INavigationContributor, \
32                            ITemplateProvider, add_notice, add_stylesheet, \
33                            add_warning
34
35
36class PreferencesModule(Component):
37    """Displays the preference panels and dispatch control to the
38    individual panels"""
39
40    implements(INavigationContributor, IRequestHandler, ITemplateProvider)
41
42    panel_providers = ExtensionPoint(IPreferencePanelProvider)
43
44    # INavigationContributor methods
45
46    def get_active_navigation_item(self, req):
47        return 'prefs'
48
49    def get_navigation_items(self, req):
50        panels = self._get_panels(req)[0]
51        if panels:
52            yield 'metanav', 'prefs', tag.a(_("Preferences"),
53                                            href=req.href.prefs())
54
55    # IRequestHandler methods
56
57    def match_request(self, req):
58        match = re.match('/prefs(?:/([^/]+))?$', req.path_info)
59        if match:
60            req.args['panel_id'] = match.group(1)
61            return True
62
63    def process_request(self, req):
64        if req.is_xhr and req.method == 'POST' and 'save_prefs' in req.args:
65            self._do_save_xhr(req)
66
67        panels, providers = self._get_panels(req)
68        if not panels:
69            raise HTTPNotFound(_("No preference panels available"))
70
71        panels = []
72        child_panels = {}
73        providers = {}
74        for provider in self.panel_providers:
75            for panel in provider.get_preference_panels(req) or []:
76                if len(panel) == 3:
77                    name, label, parent = panel
78                    child_panels.setdefault(parent, []).append((name, label))
79                else:
80                    name = panel[0]
81                    panels.append(panel)
82                providers[name] = provider
83        panels = sorted(panels, key=lambda p: (p[0] or '',) + p[1:])
84
85        panel_id = req.args.get('panel_id')
86        if panel_id is None:
87            panel_id = panels[1][0] \
88                       if len(panels) > 1 and panels[0][0] == 'advanced' \
89                       else panels[0][0]
90        chosen_provider = providers.get(panel_id)
91        if not chosen_provider:
92            raise HTTPNotFound(_("Unknown preference panel '%(panel)s'",
93                                 panel=panel_id))
94
95        session_data = {'session': req.session}
96
97        # Render child preference panels.
98        chrome = Chrome(self.env)
99        children = []
100        if child_panels.get(panel_id):
101            for name, label in child_panels[panel_id]:
102                ctemplate, cdata = \
103                    providers[name].render_preference_panel(req, name)
104                cdata.update(session_data)
105                rendered = chrome.render_fragment(req, ctemplate, cdata)
106                children.append((name, label, rendered))
107
108        resp = chosen_provider.render_preference_panel(req, panel_id)
109        data = resp[1]
110
111        data.update(session_data)
112        data.update({
113            'active_panel': panel_id,
114            'panels': panels,
115            'children': children,
116        })
117
118        add_stylesheet(req, 'common/css/prefs.css')
119        return resp
120
121    # ITemplateProvider methods
122
123    def get_htdocs_dirs(self):
124        return []
125
126    def get_templates_dirs(self):
127        return [pkg_resources.resource_filename('trac.prefs', 'templates')]
128
129    # Internal methods
130
131    def _get_panels(self, req):
132        """Return a list of available preference panels."""
133        panels = []
134        providers = {}
135        for provider in self.panel_providers:
136            p = list(provider.get_preference_panels(req) or [])
137            for panel in p:
138                providers[panel[0]] = provider
139            panels += p
140
141        return panels, providers
142
143    def _do_save_xhr(self, req):
144        for key in req.args:
145            if key not in ('save_prefs', 'panel_id', '__FORM_TOKEN'):
146                req.session[key] = req.args[key]
147        req.session.save()
148        req.send_no_content()
149
150
151class AdvancedPreferencePanel(Component):
152
153    implements(IPreferencePanelProvider)
154
155    _form_fields = ('newsid',)
156
157    # IPreferencePanelProvider methods
158
159    def get_preference_panels(self, req):
160        if not req.is_authenticated:
161            yield 'advanced', _("Advanced")
162
163    def render_preference_panel(self, req, panel):
164        if req.method == 'POST':
165            if 'restore' in req.args:
166                self._do_load(req)
167            else:
168                _do_save(req, panel, self._form_fields)
169        return 'prefs_advanced.html', {'session_id': req.session.sid}
170
171    def _do_load(self, req):
172        if not req.is_authenticated:
173            oldsid = req.args.get('loadsid')
174            if oldsid:
175                req.session.get_session(oldsid)
176                add_notice(req, _("The session has been loaded."))
177
178
179class GeneralPreferencePanel(Component):
180
181    implements(IPreferencePanelProvider)
182
183    _form_fields = ('name', 'email')
184
185    # IPreferencePanelProvider methods
186
187    def get_preference_panels(self, req):
188        yield None, _("General")
189
190    def render_preference_panel(self, req, panel):
191        if req.method == 'POST':
192            _do_save(req, panel, self._form_fields)
193        return 'prefs_general.html', {}
194
195
196class LocalizationPreferencePanel(Component):
197
198    implements(IPreferencePanelProvider)
199
200    _form_fields = ('tz', 'lc_time', 'dateinfo', 'language')
201
202    # IPreferencePanelProvider methods
203
204    def get_preference_panels(self, req):
205        yield 'localization', _("Localization")
206
207    def render_preference_panel(self, req, panel):
208        if req.method == 'POST':
209            if Locale and \
210                    req.args.get('language') != req.session.get('language'):
211                # reactivate translations with new language setting
212                # when changed
213                del req.locale  # for re-negotiating locale
214                deactivate()
215                make_activable(lambda: req.locale, self.env.path)
216            _do_save(req, panel, self._form_fields)
217
218        default_timezone_id = self.config.get('trac', 'default_timezone')
219        default_timezone = get_timezone(default_timezone_id) or localtz
220        default_time_format = \
221            self.config.get('trac', 'default_dateinfo_format') or 'relative'
222        default_date_format = \
223            self.config.get('trac', 'default_date_format') or 'locale'
224
225        data = {
226            'timezones': all_timezones,
227            'timezone': get_timezone,
228            'default_timezone': default_timezone,
229            'default_time_format': default_time_format,
230            'default_date_format': default_date_format,
231            'localtz': localtz,
232            'has_babel': False,
233        }
234        if Locale:
235            locale_ids = get_available_locales()
236            locales = [Locale.parse(locale) for locale in locale_ids]
237            # use locale identifiers from get_available_locales() instead
238            # of str(locale) to prevent storing expanded locale identifier
239            # to session, e.g. zh_Hans_CN and zh_Hant_TW, since Babel 1.0.
240            # see #11258.
241            languages = sorted((id_, locale.display_name)
242                               for id_, locale in zip(locale_ids, locales))
243            default_language_id = self.config.get('trac', 'default_language')
244            default_language = get_locale_name(default_language_id) or \
245                               _("Browser's language")
246            data['locales'] = locales
247            data['languages'] = languages
248            data['default_language'] = default_language
249            data['has_babel'] = True
250        return 'prefs_localization.html', data
251
252
253class UserInterfacePreferencePanel(Component):
254
255    implements(IPreferencePanelProvider)
256
257    _request_handlers = ExtensionPoint(IRequestHandler)
258
259    _form_fields = ('accesskeys', 'default_handler','ui.auto_preview_timeout',
260                    'ui.hide_help', 'ui.use_symbols', 'wiki_fullwidth')
261
262    # IPreferencePanelProvider methods
263
264    def get_preference_panels(self, req):
265        yield 'userinterface', _("User Interface")
266
267    def render_preference_panel(self, req, panel):
268        if req.method == 'POST':
269            _do_save(req, panel, self._form_fields)
270
271        data = {
272            'project_default_handler': self._project_default_handler,
273            'valid_default_handlers': self._valid_default_handlers,
274            'default_auto_preview_timeout': self._auto_preview_timeout,
275        }
276        return 'prefs_userinterface.html', data
277
278    # Internal methods
279
280    @property
281    def _auto_preview_timeout(self):
282        return self.config.getfloat('trac', 'auto_preview_timeout') or 0
283
284    @property
285    def _project_default_handler(self):
286        return self.config.get('trac', 'default_handler') or 'WikiModule'
287
288    @lazy
289    def _valid_default_handlers(self):
290        return sorted(handler.__class__.__name__
291                      for handler in self._request_handlers
292                      if is_valid_default_handler(handler))
293
294
295def _do_save(req, panel, form_fields):
296    for field in form_fields:
297        val = req.args.get(field, '').strip()
298        if val:
299            if field == 'ui.auto_preview_timeout':
300                fval = as_float(val, default=None)
301                if fval is None or math.isinf(fval) or math.isnan(fval) \
302                        or fval < 0:
303                    add_warning(req, _("Discarded invalid value \"%(val)s\" "
304                                       "for auto preview timeout.", val=val))
305                    continue
306            if field == 'tz' and 'tz' in req.session and \
307                    val not in all_timezones:
308                del req.session[field]
309            elif field == 'newsid':
310                req.session.change_sid(val)
311            else:
312                req.session[field] = val
313        elif (field in req.args or field + '_cb' in req.args) and \
314                field in req.session:
315            del req.session[field]
316    add_notice(req, _("Your preferences have been saved."))
317    req.redirect(req.href.prefs(panel))
318