# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # config.py: configuration utilities # # ----------------------------------------------------------------------- import sys import os import ConfigParser import fnmatch import vclib import vclib.ccvs import vclib.svn import cvsdb import viewvc import copy from viewvcmagic import ContentMagic ######################################################################### # # CONFIGURATION # ------------- # # There are three forms of configuration: # # 1. edit the viewvc.conf created by the viewvc-install(er) # 2. as (1), but delete all unchanged entries from viewvc.conf # 3. do not use viewvc.conf and just edit the defaults in this file # # Most users will want to use (1), but there are slight speed advantages # to the other two options. Note that viewvc.conf values are a bit easier # to work with since it is raw text, rather than python literal values. # # # A WORD ABOUT OPTION LAYERING/OVERRIDES # -------------------------------------- # # ViewVC has three "layers" of configuration options: # # 1. base configuration options - very basic configuration bits # found in sections like 'general', 'options', etc. # 2. vhost overrides - these options overlay/override the base # configuration on a per-vhost basis. # 3. root overrides - these options overlay/override the base # configuration and vhost overrides on a per-root basis. # # Here's a diagram of the valid overlays/overrides: # # PER-ROOT PER-VHOST BASE # # ,-----------. ,-----------. # | vhost-*/ | | | # | general | --> | general | # | | | | # `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | options | --> | options | --> | options | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | templates | --> | templates | --> | templates | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | utilities | --> | utilities | --> | utilities | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. # | vhost-*/ | | | # | cvsdb | --> | cvsdb | # | | | | # `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | authz-* | --> | authz-* | --> | authz-* | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. # | | # | vhosts | # | | # `-----------' # ,-----------. # | | # | query | # | | # `-----------' # # ### TODO: Figure out what this all means for the 'kv' stuff. # ######################################################################### class Config: _base_sections = ( # Base configuration sections. 'authz-*', 'cvsdb', 'general', 'options', 'query', 'templates', 'utilities', ) _force_multi_value = ( # Configuration values with multiple, comma-separated values. 'allowed_views', 'binary_mime_types', 'custom_log_formatting', 'cvs_roots', 'kv_files', 'languages', 'mime_types_files', 'root_parents', 'svn_roots', ) _allowed_overrides = { # Mapping of override types to allowed overridable sections. 'vhost' : ('authz-*', 'cvsdb', 'general', 'options', 'templates', 'utilities', ), 'root' : ('authz-*', 'options', 'templates', 'utilities', ) } def __init__(self): self.__guesser = None self.__root_configs = {} self.__parent = None self.root_options_overlayed = 0 for section in self._base_sections: if section[-1] == '*': continue setattr(self, section, _sub_config()) def load_config(self, pathname, vhost=None): """Load the configuration file at PATHNAME, applying configuration settings there as overrides to the built-in default values. If VHOST is provided, also process the configuration overrides specific to that virtual host.""" self.conf_path = os.path.isfile(pathname) and pathname or None self.base = os.path.dirname(pathname) self.parser = ConfigParser.ConfigParser() self.parser.optionxform = lambda x: x # don't case-normalize option names. self.parser.read(self.conf_path or []) for section in self.parser.sections(): if self._is_allowed_section(section, self._base_sections): self._process_section(self.parser, section, section) if vhost and self.parser.has_section('vhosts'): self._process_vhost(self.parser, vhost) def load_kv_files(self, language): """Process the key/value (kv) files specified in the configuration, merging their values into the configuration as dotted heirarchical items.""" kv = _sub_config() for fname in self.general.kv_files: if fname[0] == '[': idx = fname.index(']') parts = fname[1:idx].split('.') fname = fname[idx+1:].strip() else: parts = [ ] fname = fname.replace('%lang%', language) parser = ConfigParser.ConfigParser() parser.optionxform = lambda x: x # don't case-normalize option names. parser.read(os.path.join(self.base, fname)) for section in parser.sections(): for option in parser.options(section): full_name = parts + [section] ob = kv for name in full_name: try: ob = getattr(ob, name) except AttributeError: c = _sub_config() setattr(ob, name, c) ob = c setattr(ob, option, parser.get(section, option)) return kv def path(self, path): """Return PATH relative to the config file directory.""" return os.path.join(self.base, path) def _process_section(self, parser, section, subcfg_name): if not hasattr(self, subcfg_name): setattr(self, subcfg_name, _sub_config()) sc = getattr(self, subcfg_name) for opt in parser.options(section): value = parser.get(section, opt) if opt in self._force_multi_value: value = map(lambda x: x.strip(), filter(None, value.split(','))) else: try: value = int(value) except ValueError: pass ### FIXME: This feels like unnecessary depth of knowledge for a ### semi-generic configuration object. if opt == 'cvs_roots' or opt == 'svn_roots': value = _parse_roots(opt, value) setattr(sc, opt, value) def _is_allowed_section(self, section, allowed_sections): """Return 1 iff SECTION is an allowed section, defined as being explicitly present in the ALLOWED_SECTIONS list or present in the form 'someprefix-*' in that list.""" for allowed_section in allowed_sections: if allowed_section[-1] == '*': if _startswith(section, allowed_section[:-1]): return 1 elif allowed_section == section: return 1 return 0 def _is_allowed_override(self, sectype, secspec, section): """Test if SECTION is an allowed override section for sections of type SECTYPE ('vhosts' or 'root', currently) and type-specifier SECSPEC (a rootname or vhostname, currently). If it is, return the overridden base section name. If it's not an override section at all, return None. And if it's an override section but not an allowed one, raise IllegalOverrideSection.""" cv = '%s-%s/' % (sectype, secspec) lcv = len(cv) if section[:lcv] != cv: return None base_section = section[lcv:] if self._is_allowed_section(base_section, self._allowed_overrides[sectype]): return base_section raise IllegalOverrideSection(sectype, section) def _process_vhost(self, parser, vhost): # Find a vhost name for this VHOST, if any (else, we've nothing to do). canon_vhost = self._find_canon_vhost(parser, vhost) if not canon_vhost: return # Overlay any option sections associated with this vhost name. for section in parser.sections(): base_section = self._is_allowed_override('vhost', canon_vhost, section) if base_section: self._process_section(parser, section, base_section) def _find_canon_vhost(self, parser, vhost): vhost = vhost.lower().split(':')[0] # lower-case, no port for canon_vhost in parser.options('vhosts'): value = parser.get('vhosts', canon_vhost) patterns = map(lambda x: x.lower().strip(), filter(None, value.split(','))) for pat in patterns: if fnmatch.fnmatchcase(vhost, pat): return canon_vhost return None def get_root_config(self, rootname): """Get configuration object with per-root overrides for 'rootname'""" if self.__parent: return self.__parent.get_root_config(rootname) elif rootname in self.__root_configs: return self.__root_configs[rootname] __guesser = self.__guesser __root_configs = self.__root_configs parser = self.parser self.parser = None self.__guesser = None self.__root_configs = None sub = copy.deepcopy(self) sub.parser = parser self.parser = parser self.__guesser = __guesser self.__root_configs = __root_configs self.__root_configs[rootname] = sub sub.__parent = self sub.overlay_root_options(rootname) return sub def get_parent_config(self): """Get the parent non-overridden config.""" if self.__parent: return self.__parent return self def overlay_root_options(self, rootname): """Overlay per-root options for ROOTNAME atop the existing option set. This is a destructive change to the configuration.""" did_overlay = 0 if not self.conf_path: return for section in self.parser.sections(): base_section = self._is_allowed_override('root', rootname, section) if base_section: # We can currently only deal with root overlays happening # once, so check that we've not yet done any overlaying of # per-root options. assert(self.root_options_overlayed == 0) self._process_section(self.parser, section, base_section) did_overlay = 1 # If we actually did any overlaying, remember this fact so we # don't do it again later. if did_overlay: self.root_options_overlayed = 1 def _get_parser_items(self, parser, section): """Basically implement ConfigParser.items() for pre-Python-2.3 versions.""" try: return self.parser.items(section) except AttributeError: d = {} for option in parser.options(section): d[option] = parser.get(section, option) return d.items() def get_authorizer_params(self, authorizer=None): """Return a dictionary of parameter names and values which belong to the configured authorizer (or AUTHORIZER, if provided).""" params = {} if authorizer is None: authorizer = self.options.authorizer if authorizer: authz_section = 'authz-%s' % (self.options.authorizer) if hasattr(self, authz_section): sub_config = getattr(self, authz_section) for attr in dir(sub_config): params[attr] = getattr(sub_config, attr) params['__config'] = self return params def guesser(self): if not self.__guesser: self.__guesser = ContentMagic(self.options.encodings) return self.__guesser def set_defaults(self): "Set some default values in the configuration." self.general.cvs_roots = { } self.general.svn_roots = { } self.general.root_parents = [] self.general.default_root = '' self.general.mime_types_files = ["mimetypes.conf"] self.general.address = '' self.general.kv_files = [ ] self.general.languages = ['en-us'] self.utilities.rcs_dir = '' if sys.platform == "win32": self.utilities.cvsnt = 'cvs' else: self.utilities.cvsnt = None self.utilities.rcsfile_socket = '' self.utilities.svn = '' self.utilities.diff = '' self.utilities.cvsgraph = '' self.utilities.tika_server = '' self.utilities.tika_mime_types = '' self.options.root_as_url_component = 1 self.options.checkout_magic = 0 self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots'] self.options.authorizer = None self.options.mangle_email_addresses = 0 self.options.custom_log_formatting = [] self.options.default_file_view = "log" self.options.binary_mime_types = [] self.options.http_expiration_time = 600 self.options.generate_etags = 1 self.options.svn_ignore_mimetype = 0 self.options.svn_config_dir = None self.options.max_filesize_kbytes = 512 self.options.use_rcsparse = 0 self.options.sort_by = 'file' self.options.sort_group_dirs = 1 self.options.hide_attic = 1 self.options.hide_errorful_entries = 0 self.options.log_sort = 'date' self.options.diff_format = 'h' self.options.hide_cvsroot = 1 self.options.hr_breakable = 1 self.options.hr_funout = 1 self.options.hr_ignore_white = 0 self.options.hr_ignore_keyword_subst = 1 self.options.hr_intraline = 0 self.options.allow_compress = 0 self.options.template_dir = "templates/default" self.options.docroot = None self.options.show_subdir_lastmod = 0 self.options.show_roots_lastmod = 0 self.options.show_logs = 1 self.options.show_log_in_markup = 1 self.options.cross_copies = 1 self.options.use_localtime = 0 self.options.iso8601_timestamps = 0 self.options.short_log_len = 80 self.options.enable_syntax_coloration = 1 self.options.tabsize = 8 self.options.detect_encoding = 0 self.options.use_cvsgraph = 0 self.options.cvsgraph_conf = "cvsgraph.conf" self.options.allowed_cvsgraph_useropts = [] self.options.use_re_search = 0 self.options.dir_pagesize = 0 self.options.log_pagesize = 0 self.options.log_pagesextra = 3 self.options.limit_changes = 100 self.options.cvs_ondisk_charset = 'cp1251' self.options.encodings = 'cp1251:iso-8859-1' self.templates.diff = None self.templates.directory = None self.templates.error = None self.templates.file = None self.templates.graph = None self.templates.log = None self.templates.query = None self.templates.query_form = None self.templates.query_results = None self.templates.roots = None self.cvsdb.enabled = 0 self.cvsdb.index_content = 0 self.cvsdb.enable_snippets = 1 self.cvsdb.content_max_size = 0 self.cvsdb.host = '' self.cvsdb.port = 3306 self.cvsdb.socket = '' self.cvsdb.database_name = '' self.cvsdb.user = '' self.cvsdb.passwd = '' self.cvsdb.readonly_user = '' self.cvsdb.readonly_passwd = '' self.cvsdb.row_limit = 1000 self.cvsdb.rss_row_limit = 100 self.cvsdb.check_database_for_root = 0 self.cvsdb.fulltext_min_relevance = 0.2 self.cvsdb.sphinx_host = '' self.cvsdb.sphinx_port = 3307 self.cvsdb.sphinx_socket = '' self.cvsdb.sphinx_index = '' self.cvsdb.sphinx_preformatted_mime = 'text/(?!html|xml).*' self.cvsdb.sphinx_snippet_options = \ 'around: 15\n'\ 'limit: 200\n'\ 'before_match: \n'\ 'after_match: \n'\ 'chunk_separator: ... \n\n' self.query.viewvc_base_url = None def _startswith(somestr, substr): return somestr[:len(substr)] == substr def _parse_roots(config_name, config_value): roots = { } for root in config_value: try: name, path = root.split(':', 1) except: raise MalformedRoot(config_name, root) roots[name.strip()] = path.strip() return roots class ViewVCConfigurationError(Exception): pass class IllegalOverrideSection(ViewVCConfigurationError): def __init__(self, override_type, section_name): self.section_name = section_name self.override_type = override_type def __str__(self): return "malformed configuration: illegal %s override section: %s" \ % (self.override_type, self.section_name) class MalformedRoot(ViewVCConfigurationError): def __init__(self, config_name, value_given): Exception.__init__(self, config_name, value_given) self.config_name = config_name self.value_given = value_given def __str__(self): return "malformed configuration: '%s' uses invalid syntax: %s" \ % (self.config_name, self.value_given) class _sub_config: pass