1# -*- coding: iso-8859-1 -*-
2"""
3    MoinMoin - Multiple configuration handler and Configuration defaults class
4
5    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
6                2005-2008 MoinMoin:ThomasWaldmann.
7                2008      MoinMoin:JohannesBerg
8    @license: GNU GPL, see COPYING for details.
9"""
10
11import hashlib
12import re
13import os
14import sys
15import time
16
17from MoinMoin import log
18logging = log.getLogger(__name__)
19
20from MoinMoin import config, error, util, wikiutil, web
21from MoinMoin import datastruct
22from MoinMoin.auth import MoinAuth
23import MoinMoin.auth as authmodule
24import MoinMoin.events as events
25from MoinMoin.events import PageChangedEvent, PageRenamedEvent
26from MoinMoin.events import PageDeletedEvent, PageCopiedEvent
27from MoinMoin.events import PageRevertedEvent, FileAttachedEvent
28import MoinMoin.web.session
29from MoinMoin.packages import packLine
30from MoinMoin.security import AccessControlList
31
32_url_re_cache = None
33_farmconfig_mtime = None
34_config_cache = {}
35
36
37def _importConfigModule(name):
38    """ Import and return configuration module and its modification time
39
40    Handle all errors except ImportError, because missing file is not
41    always an error.
42
43    @param name: module name
44    @rtype: tuple
45    @return: module, modification time
46    """
47    try:
48        module = __import__(name, globals(), {})
49        mtime = os.path.getmtime(module.__file__)
50    except ImportError:
51        raise
52    except IndentationError, err:
53        logging.exception('Your source code / config file is not correctly indented!')
54        msg = """IndentationError: %(err)s
55
56The configuration files are Python modules. Therefore, whitespace is
57important. Make sure that you use only spaces, no tabs are allowed here!
58You have to use four spaces at the beginning of the line mostly.
59""" % {
60    'err': err,
61}
62        raise error.ConfigurationError(msg)
63    except Exception, err:
64        logging.exception('An exception happened.')
65        msg = '%s: %s' % (err.__class__.__name__, str(err))
66        raise error.ConfigurationError(msg)
67    return module, mtime
68
69
70def _url_re_list():
71    """ Return url matching regular expression
72
73    Import wikis list from farmconfig on the first call and compile the
74    regexes. Later just return the cached regex list.
75
76    @rtype: list of tuples of (name, compiled re object)
77    @return: url to wiki config name matching list
78    """
79    global _url_re_cache, _farmconfig_mtime
80    if _url_re_cache is None:
81        try:
82            farmconfig, _farmconfig_mtime = _importConfigModule('farmconfig')
83        except ImportError, err:
84            if 'farmconfig' in str(err):
85                # we failed importing farmconfig
86                logging.debug("could not import farmconfig, mapping all URLs to wikiconfig")
87                _farmconfig_mtime = 0
88                _url_re_cache = [('wikiconfig', re.compile(r'.')), ] # matches everything
89            else:
90                # maybe there was a failing import statement inside farmconfig
91                raise
92        else:
93            logging.info("using farm config: %s" % os.path.abspath(farmconfig.__file__))
94            try:
95                cache = []
96                for name, regex in farmconfig.wikis:
97                    cache.append((name, re.compile(regex)))
98                _url_re_cache = cache
99            except AttributeError:
100                logging.error("required 'wikis' list missing in farmconfig")
101                msg = """
102Missing required 'wikis' list in 'farmconfig.py'.
103
104If you run a single wiki you do not need farmconfig.py. Delete it and
105use wikiconfig.py.
106"""
107                raise error.ConfigurationError(msg)
108    return _url_re_cache
109
110
111def _makeConfig(name):
112    """ Create and return a config instance
113
114    Timestamp config with either module mtime or farmconfig mtime. This
115    mtime can be used later to invalidate older caches.
116
117    @param name: module name
118    @rtype: DefaultConfig sub class instance
119    @return: new configuration instance
120    """
121    global _farmconfig_mtime
122    try:
123        module, mtime = _importConfigModule(name)
124        configClass = getattr(module, 'Config')
125        cfg = configClass(name)
126        cfg.cfg_mtime = max(mtime, _farmconfig_mtime)
127        logging.info("using wiki config: %s" % os.path.abspath(module.__file__))
128    except ImportError, err:
129        logging.exception('Could not import.')
130        msg = """ImportError: %(err)s
131
132Check that the file is in the same directory as the server script. If
133it is not, you must add the path of the directory where the file is
134located to the python path in the server script. See the comments at
135the top of the server script.
136
137Check that the configuration file name is either "wikiconfig.py" or the
138module name specified in the wikis list in farmconfig.py. Note that the
139module name does not include the ".py" suffix.
140""" % {
141    'err': err,
142}
143        raise error.ConfigurationError(msg)
144    except AttributeError, err:
145        logging.exception('An exception occurred.')
146        msg = """AttributeError: %(err)s
147
148Could not find required "Config" class in "%(name)s.py".
149
150This might happen if you are trying to use a pre 1.3 configuration file, or
151made a syntax or spelling error.
152
153Another reason for this could be a name clash. It is not possible to have
154config names like e.g. stats.py - because that collides with MoinMoin/stats/ -
155have a look into your MoinMoin code directory what other names are NOT
156possible.
157
158Please check your configuration file. As an example for correct syntax,
159use the wikiconfig.py file from the distribution.
160""" % {
161    'name': name,
162    'err': err,
163}
164        raise error.ConfigurationError(msg)
165
166    return cfg
167
168
169def _getConfigName(url):
170    """ Return config name for url or raise """
171    for name, regex in _url_re_list():
172        match = regex.match(url)
173        if match:
174            return name
175    raise error.NoConfigMatchedError
176
177
178def getConfig(url):
179    """ Return cached config instance for url or create new one
180
181    If called by many threads in the same time multiple config
182    instances might be created. The first created item will be
183    returned, using dict.setdefault.
184
185    @param url: the url from request, possibly matching specific wiki
186    @rtype: DefaultConfig subclass instance
187    @return: config object for specific wiki
188    """
189    cfgName = _getConfigName(url)
190    try:
191        cfg = _config_cache[cfgName]
192    except KeyError:
193        cfg = _makeConfig(cfgName)
194        cfg = _config_cache.setdefault(cfgName, cfg)
195    return cfg
196
197
198# This is a way to mark some text for the gettext tools so that they don't
199# get orphaned. See http://www.python.org/doc/current/lib/node278.html.
200def _(text):
201    return text
202
203
204class CacheClass:
205    """ just a container for stuff we cache """
206    pass
207
208
209class ConfigFunctionality(object):
210    """ Configuration base class with config class behaviour.
211
212        This class contains the functionality for the DefaultConfig
213        class for the benefit of the WikiConfig macro.
214    """
215
216    # attributes of this class that should not be shown
217    # in the WikiConfig() macro.
218    cfg_mtime = None
219    siteid = None
220    cache = None
221    mail_enabled = None
222    jabber_enabled = None
223    auth_can_logout = None
224    auth_have_login = None
225    auth_login_inputs = None
226    _site_plugin_lists = None
227    _iwid = None
228    _iwid_full = None
229    xapian_searchers = None
230    moinmoin_dir = None
231    # will be lazily loaded by interwiki code when needed (?)
232    shared_intermap_files = None
233
234    def __init__(self, siteid):
235        """ Init Config instance """
236        self.siteid = siteid
237        self.cache = CacheClass()
238
239        from MoinMoin.Page import ItemCache
240        self.cache.meta = ItemCache('meta')
241        self.cache.pagelists = ItemCache('pagelists')
242
243        if self.config_check_enabled:
244            self._config_check()
245
246        # define directories
247        self.moinmoin_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))
248        data_dir = os.path.normpath(self.data_dir)
249        self.data_dir = data_dir
250        for dirname in ('user', 'cache', 'plugin'):
251            name = dirname + '_dir'
252            if not getattr(self, name, None):
253                setattr(self, name, os.path.abspath(os.path.join(data_dir, dirname)))
254        # directories below cache_dir (using __dirname__ to avoid conflicts)
255        for dirname in ('session', ):
256            name = dirname + '_dir'
257            if not getattr(self, name, None):
258                setattr(self, name, os.path.abspath(os.path.join(self.cache_dir, '__%s__' % dirname)))
259
260        # Try to decode certain names which allow unicode
261        self._decode()
262
263        # After that, pre-compile some regexes
264        self.cache.page_category_regex = re.compile(self.page_category_regex, re.UNICODE)
265        self.cache.page_dict_regex = re.compile(self.page_dict_regex, re.UNICODE)
266        self.cache.page_group_regex = re.compile(self.page_group_regex, re.UNICODE)
267        self.cache.page_template_regex = re.compile(self.page_template_regex, re.UNICODE)
268
269        # the ..._regexact versions only match if nothing is left (exact match)
270        self.cache.page_category_regexact = re.compile(u'^%s$' % self.page_category_regex, re.UNICODE)
271        self.cache.page_dict_regexact = re.compile(u'^%s$' % self.page_dict_regex, re.UNICODE)
272        self.cache.page_group_regexact = re.compile(u'^%s$' % self.page_group_regex, re.UNICODE)
273        self.cache.page_template_regexact = re.compile(u'^%s$' % self.page_template_regex, re.UNICODE)
274
275        self.cache.ua_spiders = self.ua_spiders and re.compile(self.ua_spiders, re.IGNORECASE)
276
277        self._check_directories()
278
279        if not isinstance(self.superuser, list):
280            msg = """The superuser setting in your wiki configuration is not a list
281                     (e.g. ['Sample User', 'AnotherUser']).
282                     Please change it in your wiki configuration and try again."""
283            raise error.ConfigurationError(msg)
284
285        if not isinstance(self.actions_superuser, list):
286            msg = """The actions_superuser setting in your wiki configuration is not a list
287                     (e.g. ['newaccount', 'some_other_action']).
288                     Please change it in your wiki configuration and try again."""
289            raise error.ConfigurationError(msg)
290
291        # moin < 1.9 used cookie_lifetime = <float> (but converted it to int) for logged-in users and
292        # anonymous_session_lifetime = <float> or None for anon users
293        # moin >= 1.9 uses cookie_lifetime = (<float>, <float>) - first is anon, second is logged-in
294        if not (isinstance(self.cookie_lifetime, tuple) and len(self.cookie_lifetime) == 2):
295            logging.error("wiki configuration has an invalid setting: " +
296                          "cookie_lifetime = %r" % (self.cookie_lifetime, ))
297            try:
298                anon_lifetime = self.anonymous_session_lifetime
299                logging.warning("wiki configuration has an unsupported setting: " +
300                                "anonymous_session_lifetime = %r - " % anon_lifetime +
301                                "please remove it.")
302                if anon_lifetime is None:
303                    anon_lifetime = 0
304                anon_lifetime = float(anon_lifetime)
305            except:
306                # if anything goes wrong, use default value
307                anon_lifetime = 0
308            try:
309                logged_in_lifetime = int(self.cookie_lifetime)
310            except:
311                # if anything goes wrong, use default value
312                logged_in_lifetime = 12
313            self.cookie_lifetime = (anon_lifetime, logged_in_lifetime)
314            logging.warning("using cookie_lifetime = %r - " % (self.cookie_lifetime, ) +
315                            "please fix your wiki configuration.")
316
317        self._loadPluginModule()
318
319        # Preparse user dicts
320        self._fillDicts()
321
322        # Normalize values
323        self.language_default = self.language_default.lower()
324
325        # Use site name as default name-logo
326        if self.logo_string is None:
327            self.logo_string = self.sitename
328
329        # Check for needed modules
330
331        # FIXME: maybe we should do this check later, just before a
332        # chart is needed, maybe in the chart module, instead doing it
333        # for each request. But this require a large refactoring of
334        # current code.
335        if self.chart_options:
336            try:
337                import gdchart
338            except ImportError:
339                self.chart_options = None
340
341        # 'setuid' special auth method auth method can log out
342        self.auth_can_logout = ['setuid']
343        self.auth_login_inputs = []
344        found_names = []
345        for auth in self.auth:
346            if not auth.name:
347                raise error.ConfigurationError("Auth methods must have a name.")
348            if auth.name in found_names:
349                raise error.ConfigurationError("Auth method names must be unique.")
350            found_names.append(auth.name)
351            if auth.logout_possible and auth.name:
352                self.auth_can_logout.append(auth.name)
353            for input in auth.login_inputs:
354                if not input in self.auth_login_inputs:
355                    self.auth_login_inputs.append(input)
356        self.auth_have_login = len(self.auth_login_inputs) > 0
357        self.auth_methods = found_names
358
359        # internal dict for plugin `modules' lists
360        self._site_plugin_lists = {}
361
362        # we replace any string placeholders with config values
363        # e.g u'%(page_front_page)s' % self
364        self.navi_bar = [elem % self for elem in self.navi_bar]
365
366        # check if python-xapian is installed
367        if self.xapian_search:
368            try:
369                import xapian
370            except ImportError, err:
371                self.xapian_search = False
372                logging.error("xapian_search was auto-disabled because python-xapian is not installed [%s]." % str(err))
373
374        # list to cache xapian searcher objects
375        self.xapian_searchers = []
376
377        # check if mail is possible and set flag:
378        self.mail_enabled = (self.mail_smarthost is not None or self.mail_sendmail is not None) and self.mail_from
379        self.mail_enabled = self.mail_enabled and True or False
380
381        # check if jabber bot is available and set flag:
382        self.jabber_enabled = self.notification_bot_uri is not None
383
384        # if we are to use the jabber bot, instantiate a server object for future use
385        if self.jabber_enabled:
386            from xmlrpclib import Server
387            self.notification_server = Server(self.notification_bot_uri, )
388
389        # Cache variables for the properties below
390        self._iwid = self._iwid_full = self._meta_dict = None
391
392        self.cache.acl_rights_before = AccessControlList(self, [self.acl_rights_before])
393        self.cache.acl_rights_default = AccessControlList(self, [self.acl_rights_default])
394        self.cache.acl_rights_after = AccessControlList(self, [self.acl_rights_after])
395
396        action_prefix = self.url_prefix_action
397        if action_prefix is not None and action_prefix.endswith('/'): # make sure there is no trailing '/'
398            self.url_prefix_action = action_prefix[:-1]
399
400        if self.url_prefix_local is None:
401            self.url_prefix_local = self.url_prefix_static
402
403        if self.url_prefix_fckeditor is None:
404            self.url_prefix_fckeditor = self.url_prefix_local + '/applets/FCKeditor'
405
406        if self.secrets is None:  # admin did not setup a real secret, so make up something
407            self.secrets = self.calc_secrets()
408
409        secret_key_names = ['action/cache', 'wikiutil/tickets', 'xmlrpc/ProcessMail', 'xmlrpc/RemoteScript', ]
410        if self.jabber_enabled:
411            secret_key_names.append('jabberbot')
412        if self.textchas:
413            secret_key_names.append('security/textcha')
414
415        secret_min_length = 10
416        if isinstance(self.secrets, str):
417            if len(self.secrets) < secret_min_length:
418                raise error.ConfigurationError("The secrets = '...' wiki config setting is a way too short string (minimum length is %d chars)!" % (
419                    secret_min_length))
420            # for lazy people: set all required secrets to same value
421            secrets = {}
422            for key in secret_key_names:
423                secrets[key] = self.secrets
424            self.secrets = secrets
425
426        # we check if we have all secrets we need and that they have minimum length
427        for secret_key_name in secret_key_names:
428            try:
429                secret = self.secrets[secret_key_name]
430                if len(secret) < secret_min_length:
431                    raise ValueError
432            except (KeyError, ValueError):
433                raise error.ConfigurationError("You must set a (at least %d chars long) secret string for secrets['%s']!" % (
434                    secret_min_length, secret_key_name))
435
436        if self.password_scheme not in config.password_schemes_configurable:
437            raise error.ConfigurationError("not supported: password_scheme = %r" % self.password_scheme)
438
439        if self.passlib_support:
440            try:
441                from passlib.context import CryptContext
442            except ImportError, err:
443                raise error.ConfigurationError("Wiki is configured to use passlib, but importing passlib failed [%s]!" % str(err))
444            try:
445                self.cache.pwd_context = CryptContext(**self.passlib_crypt_context)
446            except (ValueError, KeyError, TypeError, UserWarning), err:
447                # ValueError: wrong configuration values
448                # KeyError: unsupported hash (seen with passlib 1.3)
449                # TypeError: configuration value has wrong type
450                raise error.ConfigurationError("passlib_crypt_context configuration is invalid [%s]." % str(err))
451        elif self.password_scheme == '{PASSLIB}':
452            raise error.ConfigurationError("passlib_support is switched off, thus you can't use password_scheme = '{PASSLIB}'.")
453
454    def calc_secrets(self):
455        """ make up some 'secret' using some config values """
456        varnames = ['data_dir', 'data_underlay_dir', 'language_default',
457                    'mail_smarthost', 'mail_from', 'page_front_page',
458                    'theme_default', 'sitename', 'logo_string',
459                    'interwikiname', 'user_homewiki', 'acl_rights_before', ]
460        secret = ''
461        for varname in varnames:
462            var = getattr(self, varname, None)
463            if isinstance(var, (str, unicode)):
464                secret += repr(var)
465        return secret
466
467    _meta_dict = None
468    def load_meta_dict(self):
469        """ The meta_dict contains meta data about the wiki instance. """
470        if self._meta_dict is None:
471            self._meta_dict = wikiutil.MetaDict(os.path.join(self.data_dir, 'meta'), self.cache_dir)
472        return self._meta_dict
473    meta_dict = property(load_meta_dict)
474
475    # lazily load iwid(_full)
476    def make_iwid_property(attr):
477        def getter(self):
478            if getattr(self, attr, None) is None:
479                self.load_IWID()
480            return getattr(self, attr)
481        return property(getter)
482    iwid = make_iwid_property("_iwid")
483    iwid_full = make_iwid_property("_iwid_full")
484
485    # lazily create a list of event handlers
486    _event_handlers = None
487    def make_event_handlers_prop():
488        def getter(self):
489            if self._event_handlers is None:
490                self._event_handlers = events.get_handlers(self)
491            return self._event_handlers
492
493        def setter(self, new_handlers):
494            self._event_handlers = new_handlers
495
496        return property(getter, setter)
497    event_handlers = make_event_handlers_prop()
498
499    def load_IWID(self):
500        """ Loads the InterWikiID of this instance. It is used to identify the instance
501            globally.
502            The IWID is available as cfg.iwid
503            The full IWID containing the interwiki name is available as cfg.iwid_full
504            This method is called by the property.
505        """
506        try:
507            iwid = self.meta_dict['IWID']
508        except KeyError:
509            iwid = util.random_string(16).encode("hex") + "-" + str(int(time.time()))
510            self.meta_dict['IWID'] = iwid
511            self.meta_dict.sync()
512
513        self._iwid = iwid
514        if self.interwikiname is not None:
515            self._iwid_full = packLine([iwid, self.interwikiname])
516        else:
517            self._iwid_full = packLine([iwid])
518
519    def _config_check(self):
520        """ Check namespace and warn about unknown names
521
522        Warn about names which are not used by DefaultConfig, except
523        modules, classes, _private or __magic__ names.
524
525        This check is disabled by default, when enabled, it will show an
526        error message with unknown names.
527        """
528        unknown = ['"%s"' % name for name in dir(self)
529                  if not name.startswith('_') and
530                  name not in DefaultConfig.__dict__ and
531                  not isinstance(getattr(self, name), (type(sys), type(DefaultConfig)))]
532        if unknown:
533            msg = """
534Unknown configuration options: %s.
535
536For more information, visit HelpOnConfiguration. Please check your
537configuration for typos before requesting support or reporting a bug.
538""" % ', '.join(unknown)
539            raise error.ConfigurationError(msg)
540
541    def _decode(self):
542        """ Try to decode certain names, ignore unicode values
543
544        Try to decode str using utf-8. If the decode fail, raise FatalError.
545
546        Certain config variables should contain unicode values, and
547        should be defined with u'text' syntax. Python decode these if
548        the file have a 'coding' line.
549
550        This will allow utf-8 users to use simple strings using, without
551        using u'string'. Other users will have to use u'string' for
552        these names, because we don't know what is the charset of the
553        config files.
554        """
555        charset = 'utf-8'
556        message = u"""
557"%(name)s" configuration variable is a string, but should be
558unicode. Use %(name)s = u"value" syntax for unicode variables.
559
560Also check your "-*- coding -*-" line at the top of your configuration
561file. It should match the actual charset of the configuration file.
562"""
563
564        decode_names = (
565            'sitename', 'interwikiname', 'user_homewiki', 'logo_string', 'navi_bar',
566            'page_front_page', 'page_category_regex', 'page_dict_regex',
567            'page_group_regex', 'page_template_regex', 'page_license_page',
568            'page_local_spelling_words', 'acl_rights_default',
569            'acl_rights_before', 'acl_rights_after', 'mail_from',
570            'quicklinks_default', 'subscribed_pages_default',
571            )
572
573        for name in decode_names:
574            attr = getattr(self, name, None)
575            if attr:
576                # Try to decode strings
577                if isinstance(attr, str):
578                    try:
579                        setattr(self, name, unicode(attr, charset))
580                    except UnicodeError:
581                        raise error.ConfigurationError(message %
582                                                       {'name': name})
583                # Look into lists and try to decode strings inside them
584                elif isinstance(attr, list):
585                    for i in xrange(len(attr)):
586                        item = attr[i]
587                        if isinstance(item, str):
588                            try:
589                                attr[i] = unicode(item, charset)
590                            except UnicodeError:
591                                raise error.ConfigurationError(message %
592                                                               {'name': name})
593
594    def _check_directories(self):
595        """ Make sure directories are accessible
596
597        Both data and underlay should exists and allow read, write and
598        execute.
599        """
600        mode = os.F_OK | os.R_OK | os.W_OK | os.X_OK
601        for attr in ('data_dir', 'data_underlay_dir'):
602            path = getattr(self, attr)
603
604            # allow an empty underlay path or None
605            if attr == 'data_underlay_dir' and not path:
606                continue
607
608            path_pages = os.path.join(path, "pages")
609            if not (os.path.isdir(path_pages) and os.access(path_pages, mode)):
610                msg = """
611%(attr)s "%(path)s" does not exist, or has incorrect ownership or
612permissions.
613
614Make sure the directory and the subdirectory "pages" are owned by the web
615server and are readable, writable and executable by the web server user
616and group.
617
618It is recommended to use absolute paths and not relative paths. Check
619also the spelling of the directory name.
620""" % {'attr': attr, 'path': path, }
621                raise error.ConfigurationError(msg)
622
623    def _loadPluginModule(self):
624        """
625        import all plugin modules
626
627        To be able to import plugin from arbitrary path, we have to load
628        the base package once using imp.load_module. Later, we can use
629        standard __import__ call to load plugins in this package.
630
631        Since each configured plugin path has unique plugins, we load the
632        plugin packages as "moin_plugin_<sha1(path)>.plugin".
633        """
634        import imp
635
636        plugin_dirs = [self.plugin_dir] + self.plugin_dirs
637        self._plugin_modules = []
638
639        try:
640            # Lock other threads while we check and import
641            imp.acquire_lock()
642            try:
643                for pdir in plugin_dirs:
644                    csum = 'p_%s' % hashlib.new('sha1', pdir).hexdigest()
645                    modname = '%s.%s' % (self.siteid, csum)
646                    # If the module is not loaded, try to load it
647                    if not modname in sys.modules:
648                        # Find module on disk and try to load - slow!
649                        abspath = os.path.abspath(pdir)
650                        parent_dir, pname = os.path.split(abspath)
651                        fp, path, info = imp.find_module(pname, [parent_dir])
652                        try:
653                            # Load the module and set in sys.modules
654                            module = imp.load_module(modname, fp, path, info)
655                            setattr(sys.modules[self.siteid], 'csum', module)
656                        finally:
657                            # Make sure fp is closed properly
658                            if fp:
659                                fp.close()
660                    if modname not in self._plugin_modules:
661                        self._plugin_modules.append(modname)
662            finally:
663                imp.release_lock()
664        except ImportError, err:
665            msg = """
666Could not import plugin package "%(path)s" because of ImportError:
667%(err)s.
668
669Make sure your data directory path is correct, check permissions, and
670that the data/plugin directory has an __init__.py file.
671""" % {
672    'path': pdir,
673    'err': str(err),
674}
675            raise error.ConfigurationError(msg)
676
677    def _fillDicts(self):
678        """ fill config dicts
679
680        Fills in missing dict keys of derived user config by copying
681        them from this base class.
682        """
683        # user checkbox defaults
684        for key, value in DefaultConfig.user_checkbox_defaults.items():
685            if key not in self.user_checkbox_defaults:
686                self.user_checkbox_defaults[key] = value
687
688    def __getitem__(self, item):
689        """ Make it possible to access a config object like a dict """
690        return getattr(self, item)
691
692
693class DefaultConfig(ConfigFunctionality):
694    """ Configuration base class with default config values
695        (added below)
696    """
697    # Do not add anything into this class. Functionality must
698    # be added above to avoid having the methods show up in
699    # the WikiConfig macro. Settings must be added below to
700    # the options dictionary.
701
702_default_backlink_method = lambda cfg, req: 'backlink' if req.user.valid else 'pagelink'
703
704
705def _default_password_checker(cfg, request, username, password,
706                              min_length=6, min_different=4):
707    """ Check if a password is secure enough.
708        We use a built-in check to get rid of the worst passwords.
709
710        We do NOT use cracklib / python-crack here any more because it is
711        not thread-safe (we experienced segmentation faults when using it).
712
713        If you don't want to check passwords, use password_checker = None.
714
715        @return: None if there is no problem with the password,
716                 some unicode object with an error msg, if the password is problematic.
717    """
718    _ = request.getText
719    # in any case, do a very simple built-in check to avoid the worst passwords
720    if len(password) < min_length:
721        return _("Password is too short.")
722    if len(set(password)) < min_different:
723        return _("Password has not enough different characters.")
724
725    username_lower = username.lower()
726    password_lower = password.lower()
727    if username in password or password in username or \
728       username_lower in password_lower or password_lower in username_lower:
729        return _("Password is too easy (password contains name or name contains password).")
730
731    keyboards = (ur"`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./", # US kbd
732                 ur"^1234567890ߴqwertzuiop�+asdfghjkl��#yxcvbnm,.-", # german kbd
733                ) # add more keyboards!
734    for kbd in keyboards:
735        rev_kbd = kbd[::-1]
736        if password in kbd or password in rev_kbd or \
737           password_lower in kbd or password_lower in rev_kbd:
738            return _("Password is too easy (keyboard sequence).")
739    return None
740
741
742class DefaultExpression(object):
743    def __init__(self, exprstr):
744        self.text = exprstr
745        self.value = eval(exprstr)
746
747
748#
749# Options that are not prefixed automatically with their
750# group name, see below (at the options dict) for more
751# information on the layout of this structure.
752#
753options_no_group_name = {
754  # =========================================================================
755  'attachment_extension': ("Mapping of attachment extensions to actions", None,
756  (
757   ('extensions_mapping',
758       {'.tdraw': {'modify': 'twikidraw'},
759        '.adraw': {'modify': 'anywikidraw'},
760       }, "file extension -> do -> action"),
761  )),
762  # ==========================================================================
763  'datastruct': ('Datastruct settings', None, (
764    ('dicts', lambda cfg, request: datastruct.WikiDicts(request),
765     "function f(cfg, request) that returns a backend which is used to access dicts definitions."),
766    ('groups', lambda cfg, request: datastruct.WikiGroups(request),
767     "function f(cfg, request) that returns a backend which is used to access groups definitions."),
768  )),
769  # ==========================================================================
770  'session': ('Session settings', "Session-related settings, see HelpOnSessions.", (
771    ('session_service', DefaultExpression('web.session.FileSessionService()'),
772     "The session service."),
773    ('cookie_name', None,
774     'The variable part of the session cookie name. (None = determine from URL, siteidmagic = use siteid, any other string = use that)'),
775    ('cookie_secure', None,
776     'Use secure cookie. (None = auto-enable secure cookie for https, True = ever use secure cookie, False = never use secure cookie).'),
777    ('cookie_httponly', False,
778     'Use a httponly cookie that can only be used by the server, not by clientside scripts.'),
779    ('cookie_domain', None,
780     'Domain used in the session cookie. (None = do not specify domain).'),
781    ('cookie_path', None,
782     'Path used in the session cookie (None = auto-detect). Please only set if you know exactly what you are doing.'),
783    ('cookie_lifetime', (0, 12),
784     'Session lifetime [h] of (anonymous, logged-in) users (see HelpOnSessions for details).'),
785  )),
786  # ==========================================================================
787  'auth': ('Authentication / Authorization / Security settings', None, (
788    ('superuser', [],
789     "List of trusted user names with wiki system administration super powers (not to be confused with ACL admin rights!). Used for e.g. software installation, language installation via SystemPagesSetup and more. See also HelpOnSuperUser."),
790    ('auth', DefaultExpression('[MoinAuth()]'),
791     "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
792    ('auth_methods_trusted', ['http', 'given', 'xmlrpc_applytoken'], # Note: 'http' auth method is currently just a redirect to 'given'
793     'authentication methods for which users should be included in the special "Trusted" ACL group.'),
794    ('secrets', None, """Either a long shared secret string used for multiple purposes or a dict {"purpose": "longsecretstring", ...} for setting up different shared secrets for different purposes. If you don't setup own secret(s), a secret string will be auto-generated from other config settings."""),
795    ('DesktopEdition',
796     False,
797     "if True, give all local users special powers - ''only use this for a local desktop wiki!''"),
798    ('SecurityPolicy',
799     None,
800     "Class object hook for implementing security restrictions or relaxations"),
801    ('actions_superuser',
802     ['newaccount',  # spam bots create tons of user accounts, so better allow it only for superuser
803     ],
804     "Restrict actions to superuser only (list of strings)"),
805    ('actions_excluded',
806     ['xmlrpc',  # we do not want wiki admins unknowingly offering xmlrpc service
807      'MyPages',  # only works when used with a non-default SecurityPolicy (e.g. autoadmin)
808      'CopyPage',  # has questionable behaviour regarding subpages a user can't read, but can copy
809      ],
810     "Exclude unwanted actions (list of strings)"),
811
812    ('allow_xslt', False,
813        "if True, enables XSLT processing via 4Suite (Note that this is DANGEROUS. It enables anyone who can edit the wiki to get '''read/write access to your filesystem as the moin process uid/gid''' and to insert '''arbitrary HTML''' into your wiki pages, which is why this setting defaults to `False` (XSLT disabled). Do not set it to other values, except if you know what you do and if you have very trusted editors only)."),
814
815    ('password_checker', DefaultExpression('_default_password_checker'),
816     'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'),
817
818    ('password_scheme', '{PASSLIB}',
819     'Either "{PASSLIB}" (default) to use passlib for creating and upgrading password hashes (see also passlib_crypt_context for passlib configuration), '
820     'or "{SSHA}" (or any other of the builtin password schemes) to not use passlib (not recommended).'),
821
822    ('passlib_support', True,
823     'If True (default), import passlib and support password hashes offered by it.'),
824
825    ('passlib_crypt_context', dict(
826        # schemes we want to support (or deprecated schemes for which we still have
827        # hashes in our storage).
828        # note: bcrypt: we did not include it as it needs additional code (that is not pure python
829        #       and thus either needs compiling or installing platform-specific binaries) and
830        #       also there was some bcrypt issue in passlib < 1.5.3.
831        #       pbkdf2_sha512: not included as it needs at least passlib 1.4.0
832        #       sha512_crypt: supported since passlib 1.3.0 (first public release)
833        schemes=["sha512_crypt", ],
834        # default scheme for creating new pw hashes (if not given, passlib uses first from schemes)
835        #default="sha512_crypt",
836        # deprecated schemes get auto-upgraded to the default scheme at login
837        # time or when setting a password (including doing a moin account pwreset).
838        # for passlib >= 1.6, giving ["auto"] means that all schemes except the default are deprecated:
839        #deprecated=["auto"],
840        # to support also older passlib versions, rather give a explicit list:
841        #deprecated=[],
842        # vary rounds parameter randomly when creating new hashes...
843        #all__vary_rounds=0.1,
844    ),
845    "passlib CryptContext arguments, see passlib docs"),
846
847    ('recovery_token_lifetime', 12,
848     'how long the password recovery token is valid [h]'),
849  )),
850  # ==========================================================================
851  'spam_leech_dos': ('Anti-Spam/Leech/DOS',
852  'These settings help limiting ressource usage and avoiding abuse.',
853  (
854    ('hosts_deny', [], "List of denied IPs; if an IP ends with a dot, it denies a whole subnet (class A, B or C)"),
855    ('surge_action_limits',
856     {# allow max. <count> <action> requests per <dt> secs
857        # action: (count, dt)
858        'all': (30, 30), # all requests (except cache/AttachFile action) count for this limit
859        'default': (30, 60), # default limit for actions without a specific limit
860        'show': (30, 60),
861        'recall': (10, 120),
862        'raw': (20, 40),  # some people use this for css
863        'diff': (30, 60),
864        'fullsearch': (10, 120),
865        'edit': (30, 300), # can be lowered after making preview different from edit
866        'rss_rc': (1, 60),
867        # The following actions are often used for images - to avoid pages with lots of images
868        # (like photo galleries) triggering surge protection, we assign rather high limits:
869        'AttachFile': (300, 30),
870        'cache': (600, 30), # cache action is very cheap/efficient
871        # special stuff to prevent someone trying lots of usernames / passwords to log in.
872        # we keep this commented / disabled so that this feature does not get activated by default
873        # (if somebody does not override surge_action_limits with own values):
874        #'auth-ip': (10, 3600),  # same remote ip (any name)
875        #'auth-name': (10, 3600),  # same name (any remote ip)
876     },
877     "Surge protection tries to deny clients causing too much load/traffic, see HelpOnConfiguration/SurgeProtection."),
878    ('surge_lockout_time', 3600, "time [s] someone gets locked out when ignoring the warnings"),
879
880    ('textchas', None,
881     "Spam protection setup using site-specific questions/answers, see HelpOnSpam."),
882    ('textchas_disabled_group', None,
883     "Name of a group of trusted users who do not get asked !TextCha questions."),
884    ('textchas_expiry_time', 600,
885     "Time [s] for a !TextCha to expire."),
886
887    ('antispam_master_url', "http://master.moinmo.in/?action=xmlrpc2",
888     "where antispam security policy fetches spam pattern updates (if it is enabled)"),
889
890    # a regex of HTTP_USER_AGENTS that should be excluded from logging
891    # and receive a FORBIDDEN for anything except viewing a page
892    # list must not contain 'java' because of twikidraw wanting to save drawing uses this useragent
893    ('ua_spiders',
894     ('archiver|bingbot|cfetch|charlotte|crawler|gigabot|googlebot|heritrix|holmes|htdig|httrack|httpunit|'
895      'intelix|jeeves|larbin|leech|libwww-perl|linkbot|linkmap|linkwalk|litefinder|mercator|'
896      'microsoft.url.control|mirror| mj12bot|msnbot|msrbot|neomo|nutbot|omniexplorer|puf|robot|scooter|seekbot|'
897      'sherlock|slurp|sitecheck|snoopy|spider|teleport|twiceler|voilabot|voyager|webreaper|wget|yeti'),
898     "A regex of HTTP_USER_AGENTs that should be excluded from logging and are not allowed to use actions."),
899
900    ('unzip_single_file_size', 2.0 * 1000 ** 2,
901     "max. size of a single file in the archive which will be extracted [bytes]"),
902    ('unzip_attachments_space', 200.0 * 1000 ** 2,
903     "max. total amount of bytes can be used to unzip files [bytes]"),
904    ('unzip_attachments_count', 101,
905     "max. number of files which are extracted from the zip file"),
906  )),
907  # ==========================================================================
908  'style': ('Style / Theme / UI related',
909  'These settings control how the wiki user interface will look like.',
910  (
911    ('sitename', u'Untitled Wiki',
912     "Short description of your wiki site, displayed below the logo on each page, and used in RSS documents as the channel title [Unicode]"),
913    ('interwikiname', None, "unique and stable InterWiki name (prefix, moniker) of the site [Unicode], or None"),
914    ('logo_string', None, "The wiki logo top of page, HTML is allowed (`<img>` is possible as well) [Unicode]"),
915    ('html_pagetitle', None, "Allows you to set a specific HTML page title (if None, it defaults to the value of `sitename`)"),
916    ('navi_bar', [u'RecentChanges', u'FindPage', u'HelpContents', ],
917     'Most important page names. Users can add more names in their quick links in user preferences. To link to URL, use `u"[[url|link title]]"`, to use a shortened name for long page name, use `u"[[LongLongPageName|title]]"`. [list of Unicode strings]'),
918
919    ('theme_default', 'modernized',
920     "the name of the theme that is used by default (see HelpOnThemes)"),
921    ('theme_force', False,
922     "if True, do not allow to change the theme"),
923
924    ('stylesheets', [],
925     "List of tuples (media, csshref) to insert after theme css, before user css, see HelpOnThemes."),
926
927    ('supplementation_page', False,
928     "if True, show a link to the supplementation page in the theme"),
929    ('supplementation_page_name', u'Discussion',
930     "default name of the supplementation (sub)page [unicode]"),
931    ('supplementation_page_template', u'DiscussionTemplate',
932     "default template used for creation of the supplementation page [unicode]"),
933
934    ('interwiki_preferred', [], "In dialogues, show those wikis at the top of the list."),
935    ('sistersites', [], "list of tuples `('WikiName', 'sisterpagelist_fetch_url')`"),
936
937    ('trail_size', 5,
938     "Number of pages in the trail of visited pages"),
939
940    ('page_footer1', '', "Custom HTML markup sent ''before'' the system footer."),
941    ('page_footer2', '', "Custom HTML markup sent ''after'' the system footer."),
942    ('page_header1', '', "Custom HTML markup sent ''before'' the system header / title area but after the body tag."),
943    ('page_header2', '', "Custom HTML markup sent ''after'' the system header / title area (and body tag)."),
944
945    ('changed_time_fmt', '%H:%M', "Time format used on Recent``Changes for page edits within the last 24 hours"),
946    ('date_fmt', '%Y-%m-%d', "System date format, used mostly in Recent``Changes"),
947    ('datetime_fmt', '%Y-%m-%d %H:%M:%S', 'Default format for dates and times (when the user has no preferences or chose the "default" date format)'),
948    ('chart_options', None, "If you have gdchart, use something like chart_options = {'width': 720, 'height': 540}"),
949
950    ('edit_bar', ['Edit', 'Comments', 'Discussion', 'Info', 'Subscribe', 'Quicklink', 'Attachments', 'ActionsMenu'],
951     'list of edit bar entries'),
952    ('history_count', (100, 200, 5, 10, 25, 50), "Number of revisions shown for info/history action (default_count_shown, max_count_shown, [other values shown as page size choices]). At least first two values (default and maximum) should be provided. If additional values are provided, user will be able to change number of items per page in the UI."),
953    ('history_paging', True, "Enable paging functionality for info action's history display."),
954
955    ('show_hosts', True,
956     "if True, show host names and IPs. Set to False to hide them."),
957    ('show_interwiki', False,
958     "if True, let the theme display your interwiki name"),
959    ('show_names', True,
960     "if True, show user names in the revision history and on Recent``Changes. Set to False to hide them."),
961    ('show_section_numbers', False,
962     'show section numbers in headings by default'),
963    ('show_timings', False, "show some timing values at bottom of a page"),
964    ('show_version', False, "show moin's version at the bottom of a page"),
965    ('show_rename_redirect', False, "if True, offer creation of redirect pages when renaming wiki pages"),
966
967    ('backlink_method', DefaultExpression('_default_backlink_method'),
968     "function determining how the (last part of the) pagename should be rendered in the title area"),
969
970    ('packagepages_actions_excluded',
971     ['setthemename',  # related to questionable theme stuff, see below
972      'copythemefile', # maybe does not work, e.g. if no fs write permissions or real theme file path is unknown to moin
973      'installplugin', # code installation, potentially dangerous
974      'renamepage',    # dangerous with hierarchical acls
975      'deletepage',    # dangerous with hierarchical acls
976      'delattachment', # dangerous, no revisioning
977     ],
978     'list with excluded package actions (e.g. because they are dangerous / questionable)'),
979
980    ('page_credits',
981     [
982       '<a href="http://moinmo.in/" title="This site uses the MoinMoin Wiki software.">MoinMoin Powered</a>',
983       '<a href="http://moinmo.in/Python" title="MoinMoin is written in Python.">Python Powered</a>',
984       '<a href="http://moinmo.in/GPL" title="MoinMoin is GPL licensed.">GPL licensed</a>',
985       '<a href="http://validator.w3.org/check?uri=referer" title="Click here to validate this page.">Valid HTML 4.01</a>',
986     ],
987     'list with html fragments with logos or strings for crediting.'),
988
989    # These icons will show in this order in the iconbar, unless they
990    # are not relevant, e.g email icon when the wiki is not configured
991    # for email.
992    ('page_iconbar', ["up", "edit", "view", "diff", "info", "subscribe", "raw", "print", ],
993     'list of icons to show in iconbar, valid values are only those in page_icons_table. Available only in classic theme.'),
994
995    # Standard buttons in the iconbar
996    ('page_icons_table',
997     {
998        # key           pagekey, querystr dict, title, icon-key
999        'diff': ('page', {'action': 'diff'}, _("Diffs"), "diff"),
1000        'info': ('page', {'action': 'info'}, _("Info"), "info"),
1001        'edit': ('page', {'action': 'edit'}, _("Edit"), "edit"),
1002        'unsubscribe': ('page', {'action': 'unsubscribe'}, _("UnSubscribe"), "unsubscribe"),
1003        'subscribe': ('page', {'action': 'subscribe'}, _("Subscribe"), "subscribe"),
1004        'raw': ('page', {'action': 'raw'}, _("Raw"), "raw"),
1005        'xml': ('page', {'action': 'show', 'mimetype': 'text/xml'}, _("XML"), "xml"),
1006        'print': ('page', {'action': 'print'}, _("Print"), "print"),
1007        'view': ('page', {}, _("View"), "view"),
1008        'up': ('page_parent_page', {}, _("Up"), "up"),
1009     },
1010     "dict of {'iconname': (url, title, icon-img-key), ...}. Available only in classic theme."),
1011    ('show_highlight_msg', False, "Show message that page has highlighted text "
1012                                  "and provide link to non-highlighted "
1013                                  "version."),
1014  )),
1015  # ==========================================================================
1016  'editor': ('Editor related', None, (
1017    ('editor_default', 'text', "Editor to use by default, 'text' or 'gui'"),
1018    ('editor_force', True, "if True, force using the default editor"),
1019    ('editor_ui', 'theonepreferred', "Editor choice shown on the user interface, 'freechoice' or 'theonepreferred'"),
1020    ('page_license_enabled', False, 'if True, show a license hint in page editor.'),
1021    ('page_license_page', u'WikiLicense', 'Page linked from the license hint. [Unicode]'),
1022    ('edit_locking', 'warn 10', "Editor locking policy: `None`, `'warn <timeout in minutes>'`, or `'lock <timeout in minutes>'`"),
1023    ('edit_ticketing', True, None),
1024    ('edit_rows', 20, "Default height of the edit box"),
1025    ('comment_required', False, "if True, only allow saving if a comment is filled in"),
1026
1027  )),
1028  # ==========================================================================
1029  'paths': ('Paths', None, (
1030    ('data_dir', './data/', "Path to the data directory containing your (locally made) wiki pages."),
1031    ('data_underlay_dir', './underlay/', "Path to the underlay directory containing distribution system and help pages."),
1032    ('cache_dir', None, "Directory for caching, by default computed from `data_dir`/cache."),
1033    ('session_dir', None, "Directory for session storage, by default computed to be `cache_dir`/__session__."),
1034    ('user_dir', None, "Directory for user storage, by default computed to be `data_dir`/user."),
1035    ('plugin_dir', None, "Plugin directory, by default computed to be `data_dir`/plugin."),
1036    ('plugin_dirs', [], "Additional plugin directories."),
1037
1038    ('docbook_html_dir', r"/usr/share/xml/docbook/stylesheet/nwalsh/html/",
1039     'Path to the directory with the Docbook to HTML XSLT files (optional, used by the docbook parser). The default value is correct for Debian Etch.'),
1040    ('shared_intermap', None,
1041     "Path to a file containing global InterWiki definitions (or a list of such filenames)"),
1042  )),
1043  # ==========================================================================
1044  'urls': ('URLs', None, (
1045    # includes the moin version number, so we can have a unlimited cache lifetime
1046    # for the static stuff. if stuff changes on version upgrade, url will change
1047    # immediately and we have no problem with stale caches.
1048    ('url_prefix_static', config.url_prefix_static,
1049     "used as the base URL for icons, css, etc. - includes the moin version number and changes on every release. This replaces the deprecated and sometimes confusing `url_prefix = '/wiki'` setting."),
1050    ('url_prefix_local', None,
1051     "used as the base URL for some Javascript - set this to a URL on same server as the wiki if your url_prefix_static points to a different server."),
1052    ('url_prefix_fckeditor', None,
1053     "used as the base URL for FCKeditor - similar to url_prefix_local, but just for FCKeditor."),
1054
1055    ('url_prefix_action', None,
1056     "Use 'action' to enable action URL generation to be compatible with robots.txt. It will generate .../action/info/PageName?action=info then. Recommended for internet wikis."),
1057
1058    ('notification_bot_uri', None, "URI of the Jabber notification bot."),
1059
1060    ('url_mappings', {},
1061     "lookup table to remap URL prefixes (dict of {{{'prefix': 'replacement'}}}); especially useful in intranets, when whole trees of externally hosted documents move around"),
1062
1063  )),
1064  # ==========================================================================
1065  'pages': ('Special page names', None, (
1066    ('page_front_page', u'LanguageSetup',
1067     "Name of the front page. We don't expect you to keep the default. Just read LanguageSetup in case you're wondering... [Unicode]"),
1068
1069    # the following regexes should match the complete name when used in free text
1070    # the group 'all' shall match all, while the group 'key' shall match the key only
1071    # e.g. CategoryFoo -> group 'all' ==  CategoryFoo, group 'key' == Foo
1072    # moin's code will add ^ / $ at beginning / end when needed
1073    ('page_category_regex', ur'(?P<all>Category(?P<key>(?!Template)\S+))',
1074     'Pagenames exactly matching this regex are regarded as Wiki categories [Unicode]'),
1075    ('page_dict_regex', ur'(?P<all>(?P<key>\S+)Dict)',
1076     'Pagenames exactly matching this regex are regarded as pages containing variable dictionary definitions [Unicode]'),
1077    ('page_group_regex', ur'(?P<all>(?P<key>\S+)Group)',
1078     'Pagenames exactly matching this regex are regarded as pages containing group definitions [Unicode]'),
1079    ('page_template_regex', ur'(?P<all>(?P<key>\S+)Template)',
1080     'Pagenames exactly matching this regex are regarded as pages containing templates for new pages [Unicode]'),
1081
1082    ('page_local_spelling_words', u'LocalSpellingWords',
1083     'Name of the page containing user-provided spellchecker words [Unicode]'),
1084  )),
1085  # ==========================================================================
1086  'user': ('User Preferences related', None, (
1087    ('quicklinks_default', [],
1088     'List of preset quicklinks for a newly created user accounts. Existing accounts are not affected by this option whereas changes in navi_bar do always affect existing accounts. Preset quicklinks can be removed by the user in the user preferences menu, navi_bar settings not.'),
1089    ('subscribed_pages_default', [],
1090     "List of pagenames used for presetting page subscriptions for newly created user accounts."),
1091
1092    ('email_subscribed_events_default',
1093     [
1094        PageChangedEvent.__name__,
1095        PageRenamedEvent.__name__,
1096        PageDeletedEvent.__name__,
1097        PageCopiedEvent.__name__,
1098        PageRevertedEvent.__name__,
1099        FileAttachedEvent.__name__,
1100     ], None),
1101    ('jabber_subscribed_events_default', [], None),
1102
1103    ('tz_offset', 0.0,
1104     "default time zone offset in hours from UTC"),
1105
1106    ('userprefs_disabled', [],
1107     "Disable the listed user preferences plugins."),
1108    ('require_email_verification', False ,
1109     "Require verification of new user accounts."),
1110  )),
1111  # ==========================================================================
1112  'various': ('Various', None, (
1113    ('bang_meta', True, 'if True, enable {{{!NoWikiName}}} markup'),
1114    ('caching_formats', ['text_html'], "output formats that are cached; set to [] to turn off caching (useful for development)"),
1115
1116    ('config_check_enabled', False, "if True, check configuration for unknown settings."),
1117
1118    ('default_markup', 'wiki', 'Default page parser / format (name of module in `MoinMoin.parser`)'),
1119
1120    ('html_head', '', "Additional <HEAD> tags, see HelpOnThemes."),
1121    ('html_head_queries', '<meta name="robots" content="noindex,nofollow">\n',
1122     "Additional <HEAD> tags for requests with query strings, like actions."),
1123    ('html_head_posts', '<meta name="robots" content="noindex,nofollow">\n',
1124     "Additional <HEAD> tags for POST requests."),
1125    ('html_head_index', '<meta name="robots" content="index,follow">\n',
1126     "Additional <HEAD> tags for some few index pages."),
1127    ('html_head_normal', '<meta name="robots" content="index,nofollow">\n',
1128     "Additional <HEAD> tags for most normal pages."),
1129
1130    ('language_default', 'en', "Default language for user interface and page content, see HelpOnLanguages."),
1131    ('language_ignore_browser', False, "if True, ignore user's browser language settings, see HelpOnLanguages."),
1132
1133    ('log_remote_addr', True,
1134     "if True, log the remote IP address (and maybe hostname)."),
1135    ('log_reverse_dns_lookups', False,
1136     "if True, do a reverse DNS lookup on page SAVE."),
1137    ('log_timing', False,
1138     "if True, add timing infos to the log output to analyse load conditions"),
1139    ('log_events_format', 1,
1140     "0 = no events logging, 1 = standard format (like <= 1.9.7) [default], 2 = extended format"),
1141
1142    # some dangerous mimetypes (we don't use "content-disposition: inline" for them when a user
1143    # downloads such attachments, because the browser might execute e.g. Javascript contained
1144    # in the HTML and steal your moin session cookie or do other nasty stuff)
1145    ('mimetypes_xss_protect',
1146     [
1147       'text/html',
1148       'image/svg+xml',
1149       'application/x-shockwave-flash',
1150       'application/xhtml+xml',
1151     ],
1152     '"content-disposition: inline" isn\'t used for them when a user downloads such attachments'),
1153
1154    ('mimetypes_embed',
1155     [
1156       'application/x-dvi',
1157       'application/postscript',
1158       'application/pdf',
1159       'application/ogg',
1160       'application/vnd.visio',
1161       'image/x-ms-bmp',
1162       'image/svg+xml',
1163       'image/tiff',
1164       'image/x-photoshop',
1165       'audio/mpeg',
1166       'audio/midi',
1167       'audio/x-wav',
1168       'video/fli',
1169       'video/mpeg',
1170       'video/quicktime',
1171       'video/x-msvideo',
1172       'chemical/x-pdb',
1173       'x-world/x-vrml',
1174     ],
1175     'mimetypes that can be embedded by the [[HelpOnMacros/EmbedObject|EmbedObject macro]]'),
1176
1177    ('refresh', None,
1178     "refresh = (minimum_delay_s, targets_allowed) enables use of `#refresh 5 PageName` processing instruction, targets_allowed must be either `'internal'` or `'external'`"),
1179    ('rss_cache', 60, "suggested caching time for Recent''''''Changes RSS, in second"),
1180
1181    ('search_results_per_page', 25, "Number of hits shown per page in the search results"),
1182
1183    ('siteid', 'default', None),
1184    ('xmlrpc_overwrite_user', True, "Overwrite authenticated user at start of xmlrpc code"),
1185  )),
1186}
1187
1188#
1189# The 'options' dict carries default MoinMoin options. The dict is a
1190# group name to tuple mapping.
1191# Each group tuple consists of the following items:
1192#   group section heading, group help text, option list
1193#
1194# where each 'option list' is a tuple or list of option tuples
1195#
1196# each option tuple consists of
1197#   option name, default value, help text
1198#
1199# All the help texts will be displayed by the WikiConfigHelp() macro.
1200#
1201# Unlike the options_no_group_name dict, option names in this dict
1202# are automatically prefixed with "group name '_'" (i.e. the name of
1203# the group they are in and an underscore), e.g. the 'hierarchic'
1204# below creates an option called "acl_hierarchic".
1205#
1206# If you need to add a complex default expression that results in an
1207# object and should not be shown in the __repr__ form in WikiConfigHelp(),
1208# you can use the DefaultExpression class, see 'auth' above for example.
1209#
1210#
1211options = {
1212    'acl': ('Access control lists',
1213    'ACLs control who may do what, see HelpOnAccessControlLists.',
1214    (
1215      ('hierarchic', False, 'True to use hierarchical ACLs'),
1216      ('rights_default', u"Trusted:read,write,delete,revert Known:read All:read",
1217       "ACL used if no ACL is specified on the page"),
1218      ('rights_before', u"",
1219       "ACL that is processed before the on-page/default ACL"),
1220      ('rights_after', u"",
1221       "ACL that is processed after the on-page/default ACL"),
1222      ('rights_valid', ['read', 'write', 'delete', 'revert', 'admin'],
1223       "Valid tokens for right sides of ACL entries."),
1224    )),
1225
1226    'xapian': ('Xapian search', "Configuration of the Xapian based indexed search, see HelpOnXapian.", (
1227      ('search', False,
1228       "True to enable the fast, indexed search (based on the Xapian search library)"),
1229      ('index_dir', None,
1230       "Directory where the Xapian search index is stored (None = auto-configure wiki local storage)"),
1231      ('stemming', False,
1232       "True to enable Xapian word stemmer usage for indexing / searching."),
1233      ('index_history', False,
1234       "True to enable indexing of non-current page revisions."),
1235    )),
1236
1237    'user': ('Users / User settings', None, (
1238      ('email_unique', True,
1239       "if True, check email addresses for uniqueness and don't accept duplicates."),
1240      ('jid_unique', True,
1241       "if True, check Jabber IDs for uniqueness and don't accept duplicates."),
1242
1243      ('homewiki', u'Self',
1244       "interwiki name of the wiki where the user home pages are located [Unicode] - useful if you have ''many'' users. You could even link to nonwiki \"user pages\" if the wiki username is in the target URL."),
1245
1246      ('checkbox_fields',
1247       [
1248        ('mailto_author', lambda _: _('Publish my email (not my wiki homepage) in author info')),
1249        ('edit_on_doubleclick', lambda _: _('Open editor on double click')),
1250        ('remember_last_visit', lambda _: _('After login, jump to last visited page')),
1251        ('show_comments', lambda _: _('Show comment sections')),
1252        ('show_nonexist_qm', lambda _: _('Show question mark for non-existing pagelinks')),
1253        ('show_page_trail', lambda _: _('Show page trail')),
1254        ('show_toolbar', lambda _: _('Show icon toolbar')),
1255        ('show_topbottom', lambda _: _('Show top/bottom links in headings')),
1256        ('show_fancy_diff', lambda _: _('Show fancy diffs')),
1257        ('wikiname_add_spaces', lambda _: _('Add spaces to displayed wiki names')),
1258        ('remember_me', lambda _: _('Remember login information')),
1259
1260        ('disabled', lambda _: _('Disable this account forever')),
1261        # if an account is disabled, it may be used for looking up
1262        # id -> username for page info and recent changes, but it
1263        # is not usable for the user any more:
1264       ],
1265       "Describes user preferences, see HelpOnConfiguration/UserPreferences."),
1266
1267      ('checkbox_defaults',
1268       {
1269        'mailto_author': 0,
1270        'edit_on_doubleclick': 1,
1271        'remember_last_visit': 0,
1272        'show_comments': 0,
1273        'show_nonexist_qm': False,
1274        'show_page_trail': 1,
1275        'show_toolbar': 1,
1276        'show_topbottom': 0,
1277        'show_fancy_diff': 1,
1278        'wikiname_add_spaces': 0,
1279        'remember_me': 1,
1280       },
1281       "Defaults for user preferences, see HelpOnConfiguration/UserPreferences."),
1282
1283      ('checkbox_disable', [],
1284       "Disable user preferences, see HelpOnConfiguration/UserPreferences."),
1285
1286      ('checkbox_remove', [],
1287       "Remove user preferences, see HelpOnConfiguration/UserPreferences."),
1288
1289      ('form_fields',
1290       [
1291        ('name', _('Name'), "text", "36", _("(Use FirstnameLastname)")),
1292        ('aliasname', _('Alias-Name'), "text", "36", ''),
1293        ('email', _('Email'), "text", "36", ''),
1294        ('jid', _('Jabber ID'), "text", "36", ''),
1295        ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
1296        ('edit_rows', _('Editor size'), "text", "3", ''),
1297       ],
1298       None),
1299
1300      ('form_defaults',
1301       {# key: default - do NOT remove keys from here!
1302        'name': '',
1303        'aliasname': '',
1304        'password': '',
1305        'password2': '',
1306        'email': '',
1307        'jid': '',
1308        'css_url': '',
1309        'edit_rows': "20",
1310       },
1311       None),
1312
1313      ('form_disable', [], "list of field names used to disable user preferences form fields"),
1314
1315      ('form_remove', [], "list of field names used to remove user preferences form fields"),
1316
1317      ('transient_fields',
1318       ['id', 'valid', 'may', 'auth_username', 'password', 'password2', 'auth_method', 'auth_attribs', ],
1319       "User object attributes that are not persisted to permanent storage (internal use)."),
1320    )),
1321
1322    'openidrp': ('OpenID Relying Party',
1323        'These settings control the built-in OpenID Relying Party (client).',
1324    (
1325      ('allowed_op', [], "List of forced providers"),
1326    )),
1327
1328    'openid_server': ('OpenID Server',
1329        'These settings control the built-in OpenID Identity Provider (server).',
1330    (
1331      ('enabled', False, "True to enable the built-in OpenID server."),
1332      ('restricted_users_group', None, "If set to a group name, the group members are allowed to use the wiki as an OpenID provider. (None = allow for all users)"),
1333      ('enable_user', False, "If True, the OpenIDUser processing instruction is allowed."),
1334    )),
1335
1336    'mail': ('Mail settings',
1337        'These settings control outgoing and incoming email from and to the wiki.',
1338    (
1339      ('from', None, "Used as From: address for generated mail."),
1340      ('login', None, "'username userpass' for SMTP server authentication (None = don't use auth)."),
1341      ('smarthost', None, "Address of SMTP server to use for sending mail (None = don't use SMTP server)."),
1342      ('sendmail', None, "sendmail command to use for sending mail (None = don't use sendmail)"),
1343
1344      ('import_subpage_template', u"$from-$date-$subject", "Create subpages using this template when importing mail."),
1345      ('import_pagename_search', ['subject', 'to', ], "Where to look for target pagename specification."),
1346      ('import_pagename_envelope', u"%s", "Use this to add some fixed prefix/postfix to the generated target pagename."),
1347      ('import_pagename_regex', r'\[\[([^\]]*)\]\]', "Regular expression used to search for target pagename specification."),
1348      ('import_wiki_addrs', [], "Target mail addresses to consider when importing mail"),
1349
1350      ('notify_page_text', '%(intro)s%(difflink)s\n\n%(comment)s%(diff)s',
1351       "Template for putting together the pieces for the page changed/deleted/renamed notification mail text body"),
1352      ('notify_page_changed_subject', _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s'),
1353       "Template for the page changed notification mail subject header"),
1354      ('notify_page_changed_intro',
1355       _("Dear Wiki user,\n\n"
1356         'You have subscribed to a wiki page or wiki category on "%(sitename)s" for change notification.\n\n'
1357         'The "%(pagename)s" page has been changed by %(editor)s:\n'),
1358       "Template for the page changed notification mail intro text"),
1359      ('notify_page_deleted_subject', _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s'),
1360       "Template for the page deleted notification mail subject header"),
1361      ('notify_page_deleted_intro',
1362       _("Dear wiki user,\n\n"
1363         'You have subscribed to a wiki page "%(sitename)s" for change notification.\n\n'
1364         'The page "%(pagename)s" has been deleted by %(editor)s:\n\n'),
1365       "Template for the page deleted notification mail intro text"),
1366      ('notify_page_renamed_subject', _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s'),
1367       "Template for the page renamed notification mail subject header"),
1368      ('notify_page_renamed_intro',
1369       _("Dear wiki user,\n\n"
1370         'You have subscribed to a wiki page "%(sitename)s" for change notification.\n\n'
1371         'The page "%(pagename)s" has been renamed from "%(oldname)s" by %(editor)s:\n'),
1372       "Template for the page renamed notification mail intro text"),
1373      ('notify_att_added_subject', _('[%(sitename)s] New attachment added to page %(pagename)s'),
1374       "Template for the attachment added notification mail subject header"),
1375      ('notify_att_added_intro',
1376       _("Dear Wiki user,\n\n"
1377         'You have subscribed to a wiki page "%(page_name)s" for change notification. '
1378         "An attachment has been added to that page by %(editor)s. "
1379         "Following detailed information is available:\n\n"
1380         "Attachment name: %(attach_name)s\n"
1381         "Attachment size: %(attach_size)s\n"),
1382       "Template for the attachment added notification mail intro text"),
1383      ('notify_att_removed_subject', _('[%(sitename)s] Removed attachment from page %(pagename)s'),
1384       "Template for the attachment removed notification mail subject header"),
1385      ('notify_att_removed_intro',
1386       _("Dear Wiki user,\n\n"
1387         'You have subscribed to a wiki page "%(page_name)s" for change notification. '
1388         "An attachment has been removed from that page by %(editor)s. "
1389         "Following detailed information is available:\n\n"
1390         "Attachment name: %(attach_name)s\n"
1391         "Attachment size: %(attach_size)s\n"),
1392       "Template for the attachment removed notification mail intro text"),
1393      ('notify_user_created_subject',
1394       _("[%(sitename)s] New user account created"),
1395       "Template for the user created notification mail subject header"),
1396      ('notify_user_created_intro',
1397       _('Dear Superuser, a new user has just been created on "%(sitename)s". Details follow:\n\n'
1398         '    User name: %(username)s\n'
1399         '    Email address: %(useremail)s'),
1400       "Template for the user created notification mail intro text"),
1401    )),
1402
1403    'backup': ('Backup settings',
1404        'These settings control how the backup action works and who is allowed to use it.',
1405    (
1406      ('compression', 'gz', 'What compression to use for the backup ("gz" or "bz2").'),
1407      ('users', [], 'List of trusted user names who are allowed to get a backup.'),
1408      ('include', [], 'List of pathes to backup.'),
1409      ('exclude', lambda self, filename: False, 'Function f(self, filename) that tells whether a file should be excluded from backup. By default, nothing is excluded.'),
1410    )),
1411    'rss': ('RSS settings',
1412        'These settings control RSS behaviour.',
1413    (
1414      ('items_default', 15, "Default maximum items value for RSS feed. Can be "
1415                            "changed via items URL query parameter of rss_rc "
1416                            "action."),
1417      ('items_limit', 100, "Limit for item count got via RSS (i. e. user "
1418                           "can't get more than items_limit items even via "
1419                           "changing items URL query parameter)."),
1420      ('unique', 0, "If set to 1, for each page name only one RSS item would "
1421                    "be shown. Can be changed via unique rss_rc action URL "
1422                    "query parameter."),
1423      ('diffs', 0, "Add diffs in RSS item descriptions by default. Can be "
1424                   "changed via diffs URL query parameter of rss_rc action."),
1425      ('ddiffs', 0, "If set to 1, links to diff view instead of page itself "
1426                    "would be generated by default. Can be changed via ddiffs "
1427                    "URL query parameter of rss_rc action."),
1428      ('lines_default', 20, "Default line count limit for diffs added as item "
1429                            "descriptions for RSS items. Can be changed via "
1430                            "lines URL query parameter of rss_rc action."),
1431      ('lines_limit', 100, "Limit for possible line count for diffs added as "
1432                           "item descriptions in RSS."),
1433      ('show_attachment_entries', 0, "If set to 1, items, related to "
1434                                     "attachment management, would be added to "
1435                                     "RSS feed. Can be changed via show_att "
1436                                     "URL query parameter of rss_rc action."),
1437      ('page_filter_pattern', "", "Default page filter pattern for RSS feed. "
1438                                  "Empty pattern matches to any page. Pattern "
1439                                  "beginning with circumflex is interpreted as "
1440                                  "regular expression. Pattern ending with "
1441                                  "slash matches page and all its subpages. "
1442                                  "Otherwise pattern sets specific pagename. "
1443                                  "Can be changed via page URL query parameter "
1444                                  "of rss_rc action."),
1445      ('show_page_history_link', True, "Add link to page change history "
1446                                       "RSS feed in theme."),
1447    )),
1448    'search_macro': ('Search macro settings',
1449        'Settings related to behaviour of search macros (such as FullSearch, '
1450        'FullSearchCached, PageList)',
1451    (
1452      ('parse_args', False, "Do search macro parameter parsing. In previous "
1453                            "versions of MoinMoin, whole search macro "
1454                            "parameter string had been interpreted as needle. "
1455                            "Now, to provide ability to pass additional "
1456                            "parameters, this behaviour should be changed."),
1457      ('highlight_titles', 1, "Perform title matches highlighting by default "
1458                              "in search results generated by macro."),
1459      ('highlight_pages', 1, "Add highlight parameter to links in search "
1460                             "results generated by search macros by default."),
1461    )),
1462}
1463
1464def _add_options_to_defconfig(opts, addgroup=True):
1465    for groupname in opts:
1466        group_short, group_doc, group_opts = opts[groupname]
1467        for name, default, doc in group_opts:
1468            if addgroup:
1469                name = groupname + '_' + name
1470            if isinstance(default, DefaultExpression):
1471                default = default.value
1472            setattr(DefaultConfig, name, default)
1473
1474_add_options_to_defconfig(options)
1475_add_options_to_defconfig(options_no_group_name, False)
1476
1477# remove the gettext pseudo function
1478del _
1479
1480