cubicweb/web/webconfig.py
changeset 11057 0b59724cb3f2
parent 10922 7d01c8c675a0
child 11229 0276f1246a4d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/webconfig.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,475 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""web ui configuration for cubicweb instances"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+import os
+import hmac
+from uuid import uuid4
+from os.path import join, exists, split, isdir
+from warnings import warn
+
+from six import text_type
+
+from logilab.common.decorators import cached, cachedproperty
+from logilab.common.deprecation import deprecated
+from logilab.common.configuration import merge_options
+
+from cubicweb import ConfigurationError
+from cubicweb.toolsutils import read_config
+from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options
+
+
+register_persistent_options( (
+    # site-wide only web ui configuration
+    ('site-title',
+     {'type' : 'string', 'default': 'unset title',
+      'help': _('site title'),
+      'sitewide': True, 'group': 'ui',
+      }),
+    ('main-template',
+     {'type' : 'string', 'default': 'main-template',
+      'help': _('id of main template used to render pages'),
+      'sitewide': True, 'group': 'ui',
+      }),
+    # user web ui configuration
+    ('fckeditor',
+     {'type' : 'yn', 'default': False,
+      'help': _('should html fields being edited using fckeditor (a HTML '
+                'WYSIWYG editor).  You should also select text/html as default '
+                'text format to actually get fckeditor.'),
+      'group': 'ui',
+      }),
+    # navigation configuration
+    ('page-size',
+     {'type' : 'int', 'default': 40,
+      'help': _('maximum number of objects displayed by page of results'),
+      'group': 'navigation',
+      }),
+    ('related-limit',
+     {'type' : 'int', 'default': 8,
+      'help': _('maximum number of related entities to display in the primary '
+                'view'),
+      'group': 'navigation',
+      }),
+    ('combobox-limit',
+     {'type' : 'int', 'default': 20,
+      'help': _('maximum number of entities to display in related combo box'),
+      'group': 'navigation',
+      }),
+
+    ))
+
+
+class WebConfiguration(CubicWebConfiguration):
+    """the WebConfiguration is a singleton object handling instance's
+    configuration and preferences
+    """
+    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
+    cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
+
+    options = merge_options(CubicWebConfiguration.options + (
+        ('repository-uri',
+         {'type' : 'string',
+          'default': 'inmemory://',
+          'help': 'see `cubicweb.dbapi.connect` documentation for possible value',
+          'group': 'web', 'level': 2,
+          }),
+
+        ('anonymous-user',
+         {'type' : 'string',
+          'default': None,
+          'help': 'login of the CubicWeb user account to use for anonymous user (if you want to allow anonymous)',
+          'group': 'web', 'level': 1,
+          }),
+        ('anonymous-password',
+         {'type' : 'string',
+          'default': None,
+          'help': 'password of the CubicWeb user account to use for anonymous user, '
+          'if anonymous-user is set',
+          'group': 'web', 'level': 1,
+          }),
+        ('query-log-file',
+         {'type' : 'string',
+          'default': None,
+          'help': 'web instance query log file',
+          'group': 'web', 'level': 3,
+          }),
+        # web configuration
+        ('https-url',
+         {'type' : 'string',
+          'default': None,
+          'help': 'web server root url on https. By specifying this option your '\
+          'site can be available as an http and https site. Authenticated users '\
+          'will in this case be authenticated and once done navigate through the '\
+          'https site. IMPORTANTE NOTE: to do this work, you should have your '\
+          'apache redirection include "https" as base url path so cubicweb can '\
+          'differentiate between http vs https access. For instance: \n'\
+          'RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]\n'\
+          'where the cubicweb web server is listening on port 8080.',
+          'group': 'main', 'level': 3,
+          }),
+        ('datadir-url',
+         {'type': 'string', 'default': None,
+          'help': ('base url for static data, if different from "${base-url}/data/".  '
+                   'If served from a different domain, that domain should allow '
+                   'cross-origin requests.'),
+          'group': 'web',
+          }),
+        ('auth-mode',
+         {'type' : 'choice',
+          'choices' : ('cookie', 'http'),
+          'default': 'cookie',
+          'help': 'authentication mode (cookie / http)',
+          'group': 'web', 'level': 3,
+          }),
+        ('realm',
+         {'type' : 'string',
+          'default': 'cubicweb',
+          'help': 'realm to use on HTTP authentication mode',
+          'group': 'web', 'level': 3,
+          }),
+        ('http-session-time',
+         {'type' : 'time',
+          'default': 0,
+          'help': "duration of the cookie used to store session identifier. "
+          "If 0, the cookie will expire when the user exist its browser. "
+          "Should be 0 or greater than repository\'s session-time.",
+          'group': 'web', 'level': 2,
+          }),
+        ('cleanup-anonymous-session-time',
+         {'type' : 'time',
+          'default': '5min',
+          'help': 'Same as cleanup-session-time but specific to anonymous '
+          'sessions. You can have a much smaller timeout here since it will be '
+          'transparent to the user. Default to 5min.',
+          'group': 'web', 'level': 3,
+          }),
+        ('embed-allowed',
+         {'type' : 'regexp',
+          'default': None,
+          'help': 'regular expression matching URLs that may be embeded. \
+leave it blank if you don\'t want the embedding feature, or set it to ".*" \
+if you want to allow everything',
+          'group': 'web', 'level': 3,
+          }),
+        ('submit-mail',
+         {'type' : 'string',
+          'default': None,
+          'help': ('Mail used as recipient to report bug in this instance, '
+                   'if you want this feature on'),
+          'group': 'web', 'level': 2,
+          }),
+
+        ('language-negociation',
+         {'type' : 'yn',
+          'default': True,
+          'help': 'use Accept-Language http header to try to set user '\
+          'interface\'s language according to browser defined preferences',
+          'group': 'web', 'level': 2,
+          }),
+
+        ('print-traceback',
+         {'type' : 'yn',
+          'default': CubicWebConfiguration.mode != 'system',
+          'help': 'print the traceback on the error page when an error occurred',
+          'group': 'web', 'level': 2,
+          }),
+
+        ('captcha-font-file',
+         {'type' : 'string',
+          'default': join(CubicWebConfiguration.shared_dir(), 'data', 'porkys.ttf'),
+          'help': 'True type font to use for captcha image generation (you \
+must have the python imaging library installed to use captcha)',
+          'group': 'web', 'level': 3,
+          }),
+        ('captcha-font-size',
+         {'type' : 'int',
+          'default': 25,
+          'help': 'Font size to use for captcha image generation (you must \
+have the python imaging library installed to use captcha)',
+          'group': 'web', 'level': 3,
+          }),
+
+        ('concat-resources',
+         {'type' : 'yn',
+          'default': False,
+          'help': 'use modconcat-like URLS to concat and serve JS / CSS files',
+          'group': 'web', 'level': 2,
+          }),
+        ('anonymize-jsonp-queries',
+         {'type': 'yn',
+          'default': True,
+          'help': 'anonymize the connection before executing any jsonp query.',
+          'group': 'web', 'level': 1
+          }),
+        ('generate-staticdir',
+         {'type': 'yn',
+          'default': True,
+          'help': 'Generate the static data resource directory on upgrade.',
+          'group': 'web', 'level': 2,
+          }),
+        ('staticdir-path',
+         {'type': 'string',
+          'default': None,
+          'help': 'The static data resource directory path.',
+          'group': 'web', 'level': 2,
+          }),
+        ('access-control-allow-origin',
+         {'type' : 'csv',
+          'default': (),
+          'help':('comma-separated list of allowed origin domains or "*" for any domain'),
+          'group': 'web', 'level': 2,
+          }),
+        ('access-control-allow-methods',
+         {'type' : 'csv',
+          'default': (),
+          'help': ('comma-separated list of allowed HTTP methods'),
+          'group': 'web', 'level': 2,
+          }),
+        ('access-control-max-age',
+         {'type' : 'int',
+          'default': None,
+          'help': ('maximum age of cross-origin resource sharing (in seconds)'),
+          'group': 'web', 'level': 2,
+          }),
+        ('access-control-expose-headers',
+         {'type' : 'csv',
+          'default': (),
+          'help':('comma-separated list of HTTP headers the application declare in response to a preflight request'),
+          'group': 'web', 'level': 2,
+          }),
+        ('access-control-allow-headers',
+         {'type' : 'csv',
+          'default': (),
+          'help':('comma-separated list of HTTP headers the application may set in the response'),
+          'group': 'web', 'level': 2,
+          }),
+        ))
+
+    def __init__(self, *args, **kwargs):
+        super(WebConfiguration, self).__init__(*args, **kwargs)
+        self.uiprops = None
+        self.https_uiprops = None
+        self.datadir_url = None
+        self.https_datadir_url = None
+
+    def fckeditor_installed(self):
+        if self.uiprops is None:
+            return False
+        return exists(self.uiprops.get('FCKEDITOR_PATH', ''))
+
+    def cwproperty_definitions(self):
+        for key, pdef in super(WebConfiguration, self).cwproperty_definitions():
+            if key == 'ui.fckeditor' and not self.fckeditor_installed():
+                continue
+            yield key, pdef
+
+    @deprecated('[3.22] call req.cnx.repo.get_versions() directly')
+    def vc_config(self):
+        return self.repository().get_versions()
+
+    def anonymous_user(self):
+        """return a login and password to use for anonymous users.
+
+        None may be returned for both if anonymous connection is not
+        allowed or if an empty login is used in configuration
+        """
+        try:
+            user   = self['anonymous-user'] or None
+            passwd = self['anonymous-password']
+            if user:
+                user = text_type(user)
+        except KeyError:
+            user, passwd = None, None
+        except UnicodeDecodeError:
+            raise ConfigurationError("anonymous information should only contains ascii")
+        return user, passwd
+
+    @cachedproperty
+    def _instance_salt(self):
+        """This random key/salt is used to sign content to be sent back by
+        browsers, eg. in the error report form.
+        """
+        return str(uuid4()).encode('ascii')
+
+    def sign_text(self, text):
+        """sign some text for later checking"""
+        # hmac.new expect bytes
+        if isinstance(text, text_type):
+            text = text.encode('utf-8')
+        # replace \r\n so we do not depend on whether a browser "reencode"
+        # original message using \r\n or not
+        return hmac.new(self._instance_salt,
+                        text.strip().replace(b'\r\n', b'\n')).hexdigest()
+
+    def check_text_sign(self, text, signature):
+        """check the text signature is equal to the given signature"""
+        return self.sign_text(text) == signature
+
+    def locate_resource(self, rid):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        return self._fs_locate(rid, 'data')
+
+    def locate_doc_file(self, fname):
+        """return the directory where the given resource may be found"""
+        return self._fs_locate(fname, 'wdoc')[0]
+
+    @cached
+    def _fs_path_locate(self, rid, rdirectory):
+        """return the directory where the given resource may be found"""
+        path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
+        for directory in path:
+            if exists(join(directory, rdirectory, rid)):
+                return directory
+
+    def _fs_locate(self, rid, rdirectory):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        directory = self._fs_path_locate(rid, rdirectory)
+        if directory is None:
+            return None, None
+        if rdirectory == 'data' and rid.endswith('.css'):
+            if rid == 'cubicweb.old.css':
+                # @import('cubicweb.css') in css
+                warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
+                     DeprecationWarning)
+                rid = 'cubicweb.css'
+            return self.uiprops.process_resource(join(directory, rdirectory), rid), rid
+        return join(directory, rdirectory), rid
+
+    def locate_all_files(self, rid, rdirectory='wdoc'):
+        """return all files corresponding to the given resource"""
+        path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
+        for directory in path:
+            fpath = join(directory, rdirectory, rid)
+            if exists(fpath):
+                yield join(fpath)
+
+    def load_configuration(self, **kw):
+        """load instance's configuration files"""
+        super(WebConfiguration, self).load_configuration(**kw)
+        # load external resources definition
+        self._init_base_url()
+        self._build_ui_properties()
+
+    def _init_base_url(self):
+        # normalize base url(s)
+        baseurl = self['base-url'] or self.default_base_url()
+        if baseurl and baseurl[-1] != '/':
+            baseurl += '/'
+        if not (self.repairing or self.creating):
+            self.global_set_option('base-url', baseurl)
+        self.datadir_url = self['datadir-url']
+        if self.datadir_url:
+            if self.datadir_url[-1] != '/':
+                self.datadir_url += '/'
+            if self.mode != 'test':
+                self.datadir_url += '%s/' % self.instance_md5_version()
+            self.https_datadir_url = self.datadir_url
+            return
+        httpsurl = self['https-url']
+        data_relpath = self.data_relpath()
+        if httpsurl:
+            if httpsurl[-1] != '/':
+                httpsurl += '/'
+                if not self.repairing:
+                    self.global_set_option('https-url', httpsurl)
+            self.https_datadir_url = httpsurl + data_relpath
+        self.datadir_url = baseurl + data_relpath
+
+    def data_relpath(self):
+        if self.mode == 'test':
+            return 'data/'
+        return 'data/%s/' % self.instance_md5_version()
+
+    def _build_ui_properties(self):
+        # self.datadir_url[:-1] to remove trailing /
+        from cubicweb.web.propertysheet import PropertySheet
+        cachedir = join(self.appdatahome, 'uicache')
+        self.check_writeable_uid_directory(cachedir)
+        self.uiprops = PropertySheet(
+            cachedir,
+            data=lambda x: self.datadir_url + x,
+            datadir_url=self.datadir_url[:-1])
+        self._init_uiprops(self.uiprops)
+        if self['https-url']:
+            cachedir = join(self.appdatahome, 'uicachehttps')
+            self.check_writeable_uid_directory(cachedir)
+            self.https_uiprops = PropertySheet(
+                cachedir,
+                data=lambda x: self.https_datadir_url + x,
+                datadir_url=self.https_datadir_url[:-1])
+            self._init_uiprops(self.https_uiprops)
+
+    def _init_uiprops(self, uiprops):
+        libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
+        uiprops.load(libuiprops)
+        for path in reversed([self.apphome] + self.cubes_path()):
+            self._load_ui_properties_file(uiprops, path)
+        self._load_ui_properties_file(uiprops, self.apphome)
+        datadir_url = uiprops.context['datadir_url']
+        if (datadir_url+'/cubicweb.old.css') in uiprops['STYLESHEETS']:
+            warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
+                 DeprecationWarning)
+            idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.old.css')
+            uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.css'
+        if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
+            warn('[3.20] cubicweb.reset.css is obsolete', DeprecationWarning)
+            uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+        cubicweb_js_url = datadir_url + '/cubicweb.js'
+        if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
+            uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
+
+    def _load_ui_properties_file(self, uiprops, path):
+        uipropsfile = join(path, 'uiprops.py')
+        if exists(uipropsfile):
+            self.debug('loading %s', uipropsfile)
+            uiprops.load(uipropsfile)
+
+    # static files handling ###################################################
+
+    @property
+    def static_directory(self):
+        return join(self.appdatahome, 'static')
+
+    def static_file_exists(self, rpath):
+        return exists(join(self.static_directory, rpath))
+
+    def static_file_open(self, rpath, mode='wb'):
+        staticdir = self.static_directory
+        rdir, filename = split(rpath)
+        if rdir:
+            staticdir = join(staticdir, rdir)
+            if not isdir(staticdir) and 'w' in mode:
+                os.makedirs(staticdir)
+        return open(join(staticdir, filename), mode)
+
+    def static_file_add(self, rpath, data):
+        stream = self.static_file_open(rpath)
+        stream.write(data)
+        stream.close()
+
+    def static_file_del(self, rpath):
+        if self.static_file_exists(rpath):
+            os.remove(join(self.static_directory, rpath))