1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005-2021 Edgewall Software
4# Copyright (C) 2005-2007 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 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
15import copy
16import os.path
17import re
18from configparser import (ConfigParser, NoOptionError, NoSectionError,
19                          ParsingError)
20
21from trac.admin import AdminCommandError, IAdminCommandProvider
22from trac.core import Component, ExtensionPoint, TracError, implements
23from trac.util import AtomicFile, as_bool
24from trac.util.compat import wait_for_file_mtime_change
25from trac.util.html import tag
26from trac.util.text import cleandoc, printout, to_unicode, to_utf8
27from trac.util.translation import N_, _, dgettext, tag_
28
29__all__ = ['Configuration', 'ConfigSection', 'Option', 'BoolOption',
30           'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption',
31           'PathOption', 'ExtensionOption', 'OrderedExtensionsOption',
32           'ConfigurationError']
33
34_use_default = object()
35
36
37def _getint(value):
38    return int(value or 0)
39
40
41def _getfloat(value):
42    return float(value or 0.0)
43
44
45def _getlist(value, sep, keep_empty):
46    if not value:
47        return []
48    if isinstance(value, str):
49        if isinstance(sep, (list, tuple)):
50            splitted = re.split('|'.join(map(re.escape, sep)), value)
51        else:
52            splitted = value.split(sep)
53        items = [item.strip() for item in splitted]
54    else:
55        items = list(value)
56    if not keep_empty:
57        items = [item for item in items if item not in (None, '')]
58    return items
59
60
61def _getdoc(option_or_section):
62    doc = to_unicode(option_or_section.__doc__)
63    if doc:
64        doc = dgettext(option_or_section.doc_domain, doc,
65                       **(option_or_section.doc_args or {}))
66    return doc
67
68
69class ConfigurationError(TracError):
70    """Exception raised when a value in the configuration file is not valid."""
71    title = N_("Configuration Error")
72
73    def __init__(self, message=None, title=None, show_traceback=False):
74        if message is None:
75            message = _("Look in the Trac log for more information.")
76        super().__init__(message, title, show_traceback)
77
78
79class UnicodeConfigParser(ConfigParser):
80    """A Unicode-aware version of ConfigParser. Arguments are encoded to
81    UTF-8 and return values are decoded from UTF-8.
82    """
83
84    # All of the methods of ConfigParser are overridden except
85    # `getboolean`, `getint`, `getfloat`, `defaults`, `read`, `readfp`,
86    # `optionxform` and `write`. `getboolean`, `getint` and `getfloat`
87    # call `get`, so it isn't necessary to reimplement them.
88    # The base class `RawConfigParser` doesn't inherit from `object`
89    # so we can't use `super`.
90
91    def __init__(self, ignorecase_option=True, **kwargs):
92        self._ignorecase_option = ignorecase_option
93        kwargs.setdefault('interpolation', None)
94        ConfigParser.__init__(self, **kwargs)
95
96    def optionxform(self, option):
97        if self._ignorecase_option:
98            option = option.lower()
99        return option
100
101    def get(self, section, option, raw=False, vars=None,
102            fallback=_use_default):
103        try:
104            return ConfigParser.get(self, section, option, raw=raw, vars=vars)
105        except (NoSectionError, NoOptionError):
106            if fallback is _use_default:
107                raise
108            return fallback
109
110    def items(self, section, raw=False, vars=None, fallback=_use_default):
111        try:
112            return ConfigParser.items(self, section, raw=raw, vars=vars)
113        except NoSectionError:
114            if fallback is _use_default:
115                raise
116            return fallback
117
118    def set(self, section, option, value=None):
119        value_str = to_unicode(value if value is not None else '')
120        ConfigParser.set(self, section, option, value_str)
121
122    def read(self, filename, encoding='utf-8'):
123        return ConfigParser.read(self, filename, encoding)
124
125    def __copy__(self):
126        parser = self.__class__()
127        parser._sections = copy.copy(self._sections)
128        return parser
129
130    def __deepcopy__(self, memo):
131        parser = self.__class__()
132        parser._sections = copy.deepcopy(self._sections)
133        return parser
134
135
136class Configuration(object):
137    """Thin layer over `ConfigParser` from the Python standard library.
138
139    In addition to providing some convenience methods, the class remembers
140    the last modification time of the configuration file, and reparses it
141    when the file has changed.
142    """
143    def __init__(self, filename, params={}):
144        self.filename = filename
145        self.parser = UnicodeConfigParser()
146        self._pristine_parser = None
147        self.parents = []
148        self._lastmtime = 0
149        self._sections = {}
150        self.parse_if_needed(force=True)
151
152    def __repr__(self):
153        return '<%s %r>' % (self.__class__.__name__, self.filename)
154
155    def __contains__(self, name):
156        """Return whether the configuration contains a section of the given
157        name.
158        """
159        return name in self.sections()
160
161    def __getitem__(self, name):
162        """Return the configuration section with the specified name."""
163        if name not in self._sections:
164            self._sections[name] = Section(self, name)
165        return self._sections[name]
166
167    def __delitem__(self, name):
168        self._sections.pop(name, None)
169        self.parser.remove_section(name)
170
171    @property
172    def exists(self):
173        """Return boolean indicating configuration file existence.
174
175        :since: 1.0.11
176        """
177        return os.path.isfile(self.filename)
178
179    def get(self, section, key, default=''):
180        """Return the value of the specified option.
181
182        Valid default input is a string. Returns a string.
183        """
184        return self[section].get(key, default)
185
186    def getbool(self, section, key, default=''):
187        """Return the specified option as boolean value.
188
189        If the value of the option is one of "yes", "true", "enabled", "on",
190        or "1", this method wll return `True`, otherwise `False`.
191
192        Valid default input is a string or a bool. Returns a bool.
193        """
194        return self[section].getbool(key, default)
195
196    def getint(self, section, key, default=''):
197        """Return the value of the specified option as integer.
198
199        If the specified option can not be converted to an integer, a
200        `ConfigurationError` exception is raised.
201
202        Valid default input is a string or an int. Returns an int.
203        """
204        return self[section].getint(key, default)
205
206    def getfloat(self, section, key, default=''):
207        """Return the value of the specified option as float.
208
209        If the specified option can not be converted to a float, a
210        `ConfigurationError` exception is raised.
211
212        Valid default input is a string, float or int. Returns a float.
213        """
214        return self[section].getfloat(key, default)
215
216    def getlist(self, section, key, default='', sep=',', keep_empty=False):
217        """Return a list of values that have been specified as a single
218        comma-separated option.
219
220        A different separator can be specified using the `sep` parameter. The
221        `sep` parameter can specify multiple values using a list or a tuple.
222        If the `keep_empty` parameter is set to `True`, empty elements are
223        included in the list.
224
225        Valid default input is a string or a list. Returns a string.
226        """
227        return self[section].getlist(key, default, sep, keep_empty)
228
229    def getpath(self, section, key, default=''):
230        """Return a configuration value as an absolute path.
231
232        Relative paths are resolved relative to the location of this
233        configuration file.
234
235        Valid default input is a string. Returns a normalized path.
236        """
237        return self[section].getpath(key, default)
238
239    def set(self, section, key, value):
240        """Change a configuration value.
241
242        These changes are not persistent unless saved with `save()`.
243        """
244        self[section].set(key, value)
245
246    def defaults(self, compmgr=None):
247        """Returns a dictionary of the default configuration values.
248
249        If `compmgr` is specified, return only options declared in components
250        that are enabled in the given `ComponentManager`.
251        """
252        defaults = {}
253        for (section, key), option in \
254                Option.get_registry(compmgr).items():
255            defaults.setdefault(section, {})[key] = \
256                option.dumps(option.default)
257        return defaults
258
259    def options(self, section, compmgr=None):
260        """Return a list of `(name, value)` tuples for every option in the
261        specified section.
262
263        This includes options that have default values that haven't been
264        overridden. If `compmgr` is specified, only return default option
265        values for components that are enabled in the given
266        `ComponentManager`.
267        """
268        return self[section].options(compmgr)
269
270    def remove(self, section, key=None):
271        """Remove the specified option or section."""
272        if key:
273            self[section].remove(key)
274        else:
275            del self[section]
276
277    def sections(self, compmgr=None, defaults=True, empty=False):
278        """Return a list of section names.
279
280        If `compmgr` is specified, only the section names corresponding to
281        options declared in components that are enabled in the given
282        `ComponentManager` are returned.
283
284        :param empty: If `True`, include sections from the registry that
285            contain no options.
286        """
287        sections = set(self.parser.sections())
288        for parent in self.parents:
289            sections.update(parent.sections(compmgr, defaults=False))
290        if defaults:
291            sections.update(self.defaults(compmgr))
292        if empty:
293            sections.update(ConfigSection.get_registry(compmgr))
294        return sorted(sections)
295
296    def has_option(self, section, option, defaults=True):
297        """Returns True if option exists in section in either the project
298        trac.ini or one of the parents, or is available through the Option
299        registry.
300        """
301        return self[section].contains(option, defaults)
302
303    def save(self):
304        """Write the configuration options to the primary file."""
305
306        all_options = {}
307        for (section, name), option in Option.get_registry().items():
308            all_options.setdefault(section, {})[name] = option
309
310        def normalize(section, name, value):
311            option = all_options.get(section, {}).get(name)
312            return option.normalize(value) if option else value
313
314        sections = []
315        for section in self.sections():
316            options = []
317            for option in self[section]:
318                default = None
319                for parent in self.parents:
320                    if parent.has_option(section, option, defaults=False):
321                        default = normalize(section, option,
322                                            parent.get(section, option))
323                        break
324                if self.parser.has_option(section, option):
325                    current = normalize(section, option,
326                                        self.parser.get(section, option))
327                    if current != default:
328                        options.append((option, current))
329            if options:
330                sections.append((section, sorted(options)))
331
332        # Prepare new file contents to write to disk.
333        parser = UnicodeConfigParser()
334        for section, options in sections:
335            parser.add_section(section)
336            for key, val in options:
337                parser.set(section, key, val)
338
339        try:
340            self._write(parser)
341        except Exception:
342            # Revert all changes to avoid inconsistencies
343            self.parser = copy.deepcopy(self._pristine_parser)
344            raise
345        else:
346            self._pristine_parser = copy.deepcopy(self.parser)
347
348    def parse_if_needed(self, force=False):
349        if not self.filename or not self.exists:
350            return False
351
352        changed = False
353        modtime = os.path.getmtime(self.filename)
354        if force or modtime != self._lastmtime:
355            self.parser = UnicodeConfigParser()
356            try:
357                if not self.parser.read(self.filename):
358                    raise TracError(_("Error reading '%(file)s', make sure "
359                                      "it is readable.", file=self.filename))
360            except ParsingError as e:
361                raise TracError(e) from e
362            self._lastmtime = modtime
363            self._pristine_parser = copy.deepcopy(self.parser)
364            changed = True
365
366        if changed:
367            self.parents = self._get_parents()
368        else:
369            for parent in self.parents:
370                changed |= parent.parse_if_needed(force=force)
371
372        if changed:
373            self._sections = {}
374        return changed
375
376    def touch(self):
377        if self.filename and self.exists \
378                and os.access(self.filename, os.W_OK):
379            wait_for_file_mtime_change(self.filename)
380
381    def set_defaults(self, compmgr=None, component=None):
382        """Retrieve all default values and store them explicitly in the
383        configuration, so that they can be saved to file.
384
385        Values already set in the configuration are not overwritten.
386        """
387        def set_option_default(option):
388            section = option.section
389            name = option.name
390            if not self.has_option(section, name, defaults=False):
391                value = option.dumps(option.default)
392                self.set(section, name, value)
393
394        if component:
395            if component.endswith('.*'):
396                component = component[:-2]
397            component = component.lower().split('.')
398            from trac.core import ComponentMeta
399            for cls in ComponentMeta._components:
400                clsname = (cls.__module__ + '.' + cls.__name__).lower() \
401                                                               .split('.')
402                if clsname[:len(component)] == component:
403                    for option in cls.__dict__.values():
404                        if isinstance(option, Option):
405                            set_option_default(option)
406        else:
407            for option in Option.get_registry(compmgr).values():
408                set_option_default(option)
409
410    def _get_parents(self):
411        _parents = []
412        if self.parser.has_option('inherit', 'file'):
413            for filename in self.parser.get('inherit', 'file').split(','):
414                filename = filename.strip()
415                if not os.path.isabs(filename):
416                    filename = os.path.join(os.path.dirname(self.filename),
417                                            filename)
418                _parents.append(Configuration(filename))
419        return _parents
420
421    def _write(self, parser):
422        if not self.filename:
423            return
424        wait_for_file_mtime_change(self.filename)
425        with AtomicFile(self.filename, 'w') as fd:
426            fd.writelines(['# -*- coding: utf-8 -*-\n', '\n'])
427            parser.write(fd)
428
429
430class Section(object):
431    """Proxy for a specific configuration section.
432
433    Objects of this class should not be instantiated directly.
434    """
435    __slots__ = ['config', 'name', '_cache']
436
437    def __init__(self, config, name):
438        self.config = config
439        self.name = name
440        self._cache = {}
441
442    def __repr__(self):
443        return '<%s [%s]>' % (self.__class__.__name__, self.name)
444
445    def contains(self, key, defaults=True):
446        if self.config.parser.has_option(self.name, key):
447            return True
448        for parent in self.config.parents:
449            if parent[self.name].contains(key, defaults=False):
450                return True
451        return defaults and (self.name, key) in Option.registry
452
453    __contains__ = contains
454
455    def iterate(self, compmgr=None, defaults=True):
456        """Iterate over the options in this section.
457
458        If `compmgr` is specified, only return default option values for
459        components that are enabled in the given `ComponentManager`.
460        """
461        options = set()
462        if self.config.parser.has_section(self.name):
463            for option in self.config.parser.options(self.name):
464                options.add(option.lower())
465                yield option
466        for parent in self.config.parents:
467            for option in parent[self.name].iterate(defaults=False):
468                loption = option.lower()
469                if loption not in options:
470                    options.add(loption)
471                    yield option
472        if defaults:
473            for section, option in Option.get_registry(compmgr).keys():
474                if section == self.name and option.lower() not in options:
475                    yield option
476
477    __iter__ = iterate
478
479    def get(self, key, default=''):
480        """Return the value of the specified option.
481
482        Valid default input is a string. Returns a string.
483        """
484        cached = self._cache.get(key, _use_default)
485        if cached is not _use_default:
486            return cached
487        if self.config.parser.has_option(self.name, key):
488            value = self.config.parser.get(self.name, key)
489        else:
490            for parent in self.config.parents:
491                value = parent[self.name].get(key, _use_default)
492                if value is not _use_default:
493                    break
494            else:
495                if default is not _use_default:
496                    option = Option.registry.get((self.name, key))
497                    value = option.dumps(option.default) if option \
498                                                         else _use_default
499                else:
500                    value = _use_default
501        if value is _use_default:
502            return default
503        self._cache[key] = value
504        return value
505
506    def getbool(self, key, default=''):
507        """Return the value of the specified option as boolean.
508
509        This method returns `True` if the option value is one of "yes",
510        "true", "enabled", "on", or non-zero numbers, ignoring case.
511        Otherwise `False` is returned.
512
513        Valid default input is a string or a bool. Returns a bool.
514        """
515        return as_bool(self.get(key, default))
516
517    def getint(self, key, default=''):
518        """Return the value of the specified option as integer.
519
520        If the specified option can not be converted to an integer, a
521        `ConfigurationError` exception is raised.
522
523        Valid default input is a string or an int. Returns an int.
524        """
525        value = self.get(key, default)
526        try:
527            return _getint(value)
528        except ValueError:
529            raise ConfigurationError(
530                    _('[%(section)s] %(entry)s: expected integer,'
531                      ' got %(value)s', section=self.name, entry=key,
532                      value=repr(value)))
533
534    def getfloat(self, key, default=''):
535        """Return the value of the specified option as float.
536
537        If the specified option can not be converted to a float, a
538        `ConfigurationError` exception is raised.
539
540        Valid default input is a string, float or int. Returns a float.
541        """
542        value = self.get(key, default)
543        try:
544            return _getfloat(value)
545        except ValueError:
546            raise ConfigurationError(
547                    _('[%(section)s] %(entry)s: expected float,'
548                      ' got %(value)s', section=self.name, entry=key,
549                      value=repr(value)))
550
551    def getlist(self, key, default='', sep=',', keep_empty=True):
552        """Return a list of values that have been specified as a single
553        comma-separated option.
554
555        A different separator can be specified using the `sep` parameter. The
556        `sep` parameter can specify multiple values using a list or a tuple.
557        If the `keep_empty` parameter is set to `True`, empty elements are
558        included in the list.
559
560        Valid default input is a string or a list. Returns a list.
561        """
562        return _getlist(self.get(key, default), sep, keep_empty)
563
564    def getpath(self, key, default=''):
565        """Return the value of the specified option as a path, relative to
566        the location of this configuration file.
567
568        Valid default input is a string. Returns a normalized path.
569        """
570        path = self.get(key, default)
571        if not path:
572            return default
573        if not os.path.isabs(path):
574            path = os.path.join(os.path.dirname(self.config.filename), path)
575        return os.path.normcase(os.path.realpath(path))
576
577    def options(self, compmgr=None):
578        """Return `(key, value)` tuples for every option in the section.
579
580        This includes options that have default values that haven't been
581        overridden. If `compmgr` is specified, only return default option
582        values for components that are enabled in the given `ComponentManager`.
583        """
584        for key in self.iterate(compmgr):
585            yield key, self.get(key)
586
587    def set(self, key, value):
588        """Change a configuration value.
589
590        These changes are not persistent unless saved with `save()`.
591        """
592        self._cache.pop(key, None)
593        if not self.config.parser.has_section(self.name):
594            self.config.parser.add_section(self.name)
595        return self.config.parser.set(self.name, key, value)
596
597    def remove(self, key):
598        """Delete a key from this section.
599
600        Like for `set()`, the changes won't persist until `save()` gets
601        called.
602        """
603        self._cache.pop(key, None)
604        if self.config.parser.has_section(self.name):
605            self.config.parser.remove_option(self.name, key)
606            if not self.config.parser.options(self.name):
607                del self.config[self.name]
608
609
610def _get_registry(cls, compmgr=None):
611    """Return the descriptor registry.
612
613    If `compmgr` is specified, only return descriptors for components that
614    are enabled in the given `ComponentManager`.
615    """
616    if compmgr is None:
617        return cls.registry
618
619    from trac.core import ComponentMeta
620    components = {}
621    for comp in ComponentMeta._components:
622        for attr in comp.__dict__.values():
623            if isinstance(attr, cls):
624                components[attr] = comp
625
626    return dict(each for each in cls.registry.items()
627                if each[1] not in components
628                   or compmgr.is_enabled(components[each[1]]))
629
630
631class ConfigSection(object):
632    """Descriptor for configuration sections."""
633
634    registry = {}
635
636    @staticmethod
637    def get_registry(compmgr=None):
638        """Return the section registry, as a `dict` mapping section names to
639        `ConfigSection` objects.
640
641        If `compmgr` is specified, only return sections for components that
642        are enabled in the given `ComponentManager`.
643        """
644        return _get_registry(ConfigSection, compmgr)
645
646    def __init__(self, name, doc, doc_domain='tracini', doc_args=None):
647        """Create the configuration section."""
648        self.name = name
649        self.registry[self.name] = self
650        self.__doc__ = cleandoc(doc)
651        self.doc_domain = doc_domain
652        self.doc_args = doc_args
653
654    def __get__(self, instance, owner):
655        if instance is None:
656            return self
657        config = getattr(instance, 'config', None)
658        if config and isinstance(config, Configuration):
659            return config[self.name]
660
661    def __repr__(self):
662        return '<%s [%s]>' % (self.__class__.__name__, self.name)
663
664    @property
665    def doc(self):
666        """Return localized document of the section"""
667        return _getdoc(self)
668
669
670class Option(object):
671    """Descriptor for configuration options."""
672
673    registry = {}
674
675    def accessor(self, section, name, default):
676        return section.get(name, default)
677
678    @staticmethod
679    def get_registry(compmgr=None):
680        """Return the option registry, as a `dict` mapping `(section, key)`
681        tuples to `Option` objects.
682
683        If `compmgr` is specified, only return options for components that are
684        enabled in the given `ComponentManager`.
685        """
686        return _get_registry(Option, compmgr)
687
688    def __init__(self, section, name, default=None, doc='',
689                 doc_domain='tracini', doc_args=None):
690        """Create the configuration option.
691
692        :param section: the name of the configuration section this option
693                        belongs to
694        :param name: the name of the option
695        :param default: the default value for the option
696        :param doc: documentation of the option
697        """
698        self.section = section
699        self.name = name
700        self.default = self.normalize(default)
701        self.registry[(self.section, self.name)] = self
702        self.__doc__ = cleandoc(doc)
703        self.doc_domain = doc_domain
704        self.doc_args = doc_args
705
706    def __get__(self, instance, owner):
707        if instance is None:
708            return self
709        config = getattr(instance, 'config', None)
710        if config and isinstance(config, Configuration):
711            section = config[self.section]
712            value = self.accessor(section, self.name, self.default)
713            return value
714
715    def __set__(self, instance, value):
716        raise AttributeError(_("Setting attribute is not allowed."))
717
718    def __repr__(self):
719        return '<%s [%s] %r>' % (self.__class__.__name__, self.section,
720                                 self.name)
721
722    @property
723    def doc(self):
724        """Return localized document of the option"""
725        return _getdoc(self)
726
727    def dumps(self, value):
728        """Return the value as a string to write to a trac.ini file"""
729        if value is None:
730            return ''
731        if value is True:
732            return 'enabled'
733        if value is False:
734            return 'disabled'
735        if isinstance(value, str):
736            return value
737        return to_unicode(value)
738
739    def normalize(self, value):
740        """Normalize the given value to write to a trac.ini file"""
741        return self.dumps(value)
742
743
744class BoolOption(Option):
745    """Descriptor for boolean configuration options."""
746
747    def accessor(self, section, name, default):
748        return section.getbool(name, default)
749
750    def normalize(self, value):
751        if value not in (True, False):
752            value = as_bool(value)
753        return self.dumps(value)
754
755
756class IntOption(Option):
757    """Descriptor for integer configuration options."""
758
759    def accessor(self, section, name, default):
760        return section.getint(name, default)
761
762    def normalize(self, value):
763        try:
764            value = _getint(value)
765        except ValueError:
766            pass
767        return self.dumps(value)
768
769
770class FloatOption(Option):
771    """Descriptor for float configuration options."""
772
773    def accessor(self, section, name, default):
774        return section.getfloat(name, default)
775
776    def normalize(self, value):
777        try:
778            value = _getfloat(value)
779        except ValueError:
780            pass
781        return self.dumps(value)
782
783
784class ListOption(Option):
785    """Descriptor for configuration options that contain multiple values
786    separated by a specific character.
787    """
788
789    def __init__(self, section, name, default=None, sep=',', keep_empty=False,
790                 doc='', doc_domain='tracini', doc_args=None):
791        self.sep = sep
792        self.keep_empty = keep_empty
793        Option.__init__(self, section, name, default, doc, doc_domain,
794                        doc_args)
795
796    def accessor(self, section, name, default):
797        return section.getlist(name, default, self.sep, self.keep_empty)
798
799    def dumps(self, value):
800        if isinstance(value, (list, tuple)):
801            sep = self.sep
802            if isinstance(sep, (list, tuple)):
803                sep = sep[0]
804            return sep.join(Option.dumps(self, v) or '' for v in value)
805        return Option.dumps(self, value)
806
807    def normalize(self, value):
808        return self.dumps(_getlist(value, self.sep, self.keep_empty))
809
810
811class ChoiceOption(Option):
812    """Descriptor for configuration options providing a choice among a list
813    of items.
814
815    The default value is the first choice in the list.
816    """
817
818    def __init__(self, section, name, choices, doc='', doc_domain='tracini',
819                 doc_args=None, case_sensitive=True):
820        Option.__init__(self, section, name, to_unicode(choices[0]), doc,
821                        doc_domain, doc_args)
822        self.choices = list({to_unicode(c).strip() for c in choices})
823        self.case_sensitive = case_sensitive
824
825    def accessor(self, section, name, default):
826        value = section.get(name, default)
827        choices = self.choices[:]
828        if not self.case_sensitive:
829            choices = [c.lower() for c in choices]
830            value = value.lower()
831        try:
832            idx = choices.index(value)
833        except ValueError:
834            raise ConfigurationError(
835                    _('[%(section)s] %(entry)s: expected one of '
836                      '(%(choices)s), got %(value)s',
837                      section=section.name, entry=name, value=repr(value),
838                      choices=', '.join('"%s"' % c
839                                        for c in sorted(self.choices))))
840        return self.choices[idx]
841
842
843class PathOption(Option):
844    """Descriptor for file system path configuration options.
845
846    Relative paths are resolved to absolute paths using the directory
847    containing the configuration file as the reference.
848    """
849
850    def accessor(self, section, name, default):
851        return section.getpath(name, default)
852
853
854class ExtensionOption(Option):
855    """Name of a component implementing `interface`. Raises a
856    `ConfigurationError` if the component cannot be found in the list of
857    active components implementing the interface."""
858
859    def __init__(self, section, name, interface, default=None, doc='',
860                 doc_domain='tracini', doc_args=None):
861        Option.__init__(self, section, name, default, doc, doc_domain,
862                        doc_args)
863        self.xtnpt = ExtensionPoint(interface)
864
865    def __get__(self, instance, owner):
866        if instance is None:
867            return self
868        value = Option.__get__(self, instance, owner)
869        for impl in self.xtnpt.extensions(instance):
870            if impl.__class__.__name__ == value:
871                return impl
872        raise ConfigurationError(
873            tag_("Cannot find an implementation of the %(interface)s "
874                 "interface named %(implementation)s. Please check "
875                 "that the Component is enabled or update the option "
876                 "%(option)s in trac.ini.",
877                 interface=tag.code(self.xtnpt.interface.__name__),
878                 implementation=tag.code(value),
879                 option=tag.code("[%s] %s" % (self.section, self.name))))
880
881
882class OrderedExtensionsOption(ListOption):
883    """A comma separated, ordered, list of components implementing
884    `interface`. Can be empty.
885
886    If `include_missing` is true (the default) all components implementing the
887    interface are returned, with those specified by the option ordered first.
888    """
889
890    def __init__(self, section, name, interface, default=None,
891                 include_missing=True, doc='', doc_domain='tracini',
892                 doc_args=None):
893        ListOption.__init__(self, section, name, default, doc=doc,
894                            doc_domain=doc_domain, doc_args=doc_args)
895        self.xtnpt = ExtensionPoint(interface)
896        self.include_missing = include_missing
897
898    def __get__(self, instance, owner):
899        if instance is None:
900            return self
901        order = ListOption.__get__(self, instance, owner)
902        components = []
903        implementing_classes = []
904        for impl in self.xtnpt.extensions(instance):
905            implementing_classes.append(impl.__class__.__name__)
906            if self.include_missing or impl.__class__.__name__ in order:
907                components.append(impl)
908        not_found = sorted(set(order) - set(implementing_classes))
909        if not_found:
910            raise ConfigurationError(
911                tag_("Cannot find implementation(s) of the %(interface)s "
912                     "interface named %(implementation)s. Please check "
913                     "that the Component is enabled or update the option "
914                     "%(option)s in trac.ini.",
915                     interface=tag.code(self.xtnpt.interface.__name__),
916                     implementation=tag(
917                         (', ' if idx != 0 else None, tag.code(impl))
918                         for idx, impl in enumerate(not_found)),
919                     option=tag.code("[%s] %s" % (self.section, self.name))))
920
921        def key(impl):
922            name = impl.__class__.__name__
923            if name in order:
924                return 0, order.index(name)
925            else:
926                return 1, components.index(impl)
927        return sorted(components, key=key)
928
929
930class ConfigurationAdmin(Component):
931    """trac-admin command provider for trac.ini administration."""
932
933    implements(IAdminCommandProvider)
934
935    # IAdminCommandProvider methods
936
937    def get_admin_commands(self):
938        yield ('config get', '<section> <option>',
939               'Get the value of the given option in "trac.ini"',
940               self._complete_config, self._do_get)
941        yield ('config remove', '<section> [<option>]',
942               'Remove the specified option or section from "trac.ini"',
943               self._complete_config, self._do_remove)
944        yield ('config set', '<section> <option> <value>',
945               'Set the value for the given option in "trac.ini"',
946               self._complete_config, self._do_set)
947
948    def _complete_config(self, args):
949        if len(args) == 1:
950            return self.config.sections(empty=True)
951        elif len(args) == 2:
952            return [name for (name, value) in self.config[args[0]].options()]
953
954    def _do_get(self, section, option):
955        if not self.config.has_option(section, option):
956            raise AdminCommandError(
957                _("Option '%(option)s' doesn't exist in section"
958                  " '%(section)s'", option=option, section=section))
959        printout(self.config.get(section, option))
960
961    def _do_set(self, section, option, value):
962        self.config.set(section, option, value)
963        if section == 'components' and as_bool(value):
964            self.config.set_defaults(component=option)
965        self.config.save()
966        if section == 'inherit' and option == 'file':
967            self.config.parse_if_needed(force=True)  # Full reload
968
969    def _do_remove(self, section, option=None):
970        if option and not self.config.has_option(section, option):
971            raise AdminCommandError(
972                _("Option '%(option)s' doesn't exist in section"
973                  " '%(section)s'", option=option, section=section))
974        elif section not in self.config:
975            raise AdminCommandError(
976                _("Section '%(section)s' doesn't exist", section=section))
977        self.config.remove(section, option)
978        self.config.save()
979        if section == 'inherit' and option == 'file':
980            self.config.parse_if_needed(force=True)  # Full reload
981
982
983def get_configinfo(env):
984    """Returns a list of dictionaries containing the `name` and `options`
985    of each configuration section. The value of `options` is a list of
986    dictionaries containing the `name`, `value` and `modified` state of
987    each configuration option. The `modified` value is True if the value
988    differs from its default.
989
990    :since: version 1.1.2
991    """
992    all_options = {}
993    for (section, name), option in \
994            Option.get_registry(env.compmgr).items():
995        all_options.setdefault(section, {})[name] = option
996    sections = []
997    for section in env.config.sections(env.compmgr):
998        options = []
999        for name, value in env.config.options(section, env.compmgr):
1000            registered = all_options.get(section, {}).get(name)
1001            if registered:
1002                default = registered.default
1003                normalized = registered.normalize(value)
1004            else:
1005                default = ''
1006                normalized = str(value)
1007            options.append({'name': name, 'value': value,
1008                            'modified': normalized != default})
1009        options.sort(key=lambda o: o['name'])
1010        sections.append({'name': section, 'options': options})
1011    sections.sort(key=lambda s: s['name'])
1012    return sections
1013