web/webconfig.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """web ui configuration for cubicweb instances"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 from cubicweb import _
       
    22 
       
    23 import os
       
    24 import hmac
       
    25 from uuid import uuid4
       
    26 from os.path import join, exists, split, isdir
       
    27 from warnings import warn
       
    28 
       
    29 from six import text_type
       
    30 
       
    31 from logilab.common.decorators import cached, cachedproperty
       
    32 from logilab.common.deprecation import deprecated
       
    33 from logilab.common.configuration import merge_options
       
    34 
       
    35 from cubicweb import ConfigurationError
       
    36 from cubicweb.toolsutils import read_config
       
    37 from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options
       
    38 
       
    39 
       
    40 register_persistent_options( (
       
    41     # site-wide only web ui configuration
       
    42     ('site-title',
       
    43      {'type' : 'string', 'default': 'unset title',
       
    44       'help': _('site title'),
       
    45       'sitewide': True, 'group': 'ui',
       
    46       }),
       
    47     ('main-template',
       
    48      {'type' : 'string', 'default': 'main-template',
       
    49       'help': _('id of main template used to render pages'),
       
    50       'sitewide': True, 'group': 'ui',
       
    51       }),
       
    52     # user web ui configuration
       
    53     ('fckeditor',
       
    54      {'type' : 'yn', 'default': False,
       
    55       'help': _('should html fields being edited using fckeditor (a HTML '
       
    56                 'WYSIWYG editor).  You should also select text/html as default '
       
    57                 'text format to actually get fckeditor.'),
       
    58       'group': 'ui',
       
    59       }),
       
    60     # navigation configuration
       
    61     ('page-size',
       
    62      {'type' : 'int', 'default': 40,
       
    63       'help': _('maximum number of objects displayed by page of results'),
       
    64       'group': 'navigation',
       
    65       }),
       
    66     ('related-limit',
       
    67      {'type' : 'int', 'default': 8,
       
    68       'help': _('maximum number of related entities to display in the primary '
       
    69                 'view'),
       
    70       'group': 'navigation',
       
    71       }),
       
    72     ('combobox-limit',
       
    73      {'type' : 'int', 'default': 20,
       
    74       'help': _('maximum number of entities to display in related combo box'),
       
    75       'group': 'navigation',
       
    76       }),
       
    77 
       
    78     ))
       
    79 
       
    80 
       
    81 class WebConfiguration(CubicWebConfiguration):
       
    82     """the WebConfiguration is a singleton object handling instance's
       
    83     configuration and preferences
       
    84     """
       
    85     cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
       
    86     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
       
    87 
       
    88     options = merge_options(CubicWebConfiguration.options + (
       
    89         ('repository-uri',
       
    90          {'type' : 'string',
       
    91           'default': 'inmemory://',
       
    92           'help': 'see `cubicweb.dbapi.connect` documentation for possible value',
       
    93           'group': 'web', 'level': 2,
       
    94           }),
       
    95 
       
    96         ('anonymous-user',
       
    97          {'type' : 'string',
       
    98           'default': None,
       
    99           'help': 'login of the CubicWeb user account to use for anonymous user (if you want to allow anonymous)',
       
   100           'group': 'web', 'level': 1,
       
   101           }),
       
   102         ('anonymous-password',
       
   103          {'type' : 'string',
       
   104           'default': None,
       
   105           'help': 'password of the CubicWeb user account to use for anonymous user, '
       
   106           'if anonymous-user is set',
       
   107           'group': 'web', 'level': 1,
       
   108           }),
       
   109         ('query-log-file',
       
   110          {'type' : 'string',
       
   111           'default': None,
       
   112           'help': 'web instance query log file',
       
   113           'group': 'web', 'level': 3,
       
   114           }),
       
   115         # web configuration
       
   116         ('https-url',
       
   117          {'type' : 'string',
       
   118           'default': None,
       
   119           'help': 'web server root url on https. By specifying this option your '\
       
   120           'site can be available as an http and https site. Authenticated users '\
       
   121           'will in this case be authenticated and once done navigate through the '\
       
   122           'https site. IMPORTANTE NOTE: to do this work, you should have your '\
       
   123           'apache redirection include "https" as base url path so cubicweb can '\
       
   124           'differentiate between http vs https access. For instance: \n'\
       
   125           'RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]\n'\
       
   126           'where the cubicweb web server is listening on port 8080.',
       
   127           'group': 'main', 'level': 3,
       
   128           }),
       
   129         ('datadir-url',
       
   130          {'type': 'string', 'default': None,
       
   131           'help': ('base url for static data, if different from "${base-url}/data/".  '
       
   132                    'If served from a different domain, that domain should allow '
       
   133                    'cross-origin requests.'),
       
   134           'group': 'web',
       
   135           }),
       
   136         ('auth-mode',
       
   137          {'type' : 'choice',
       
   138           'choices' : ('cookie', 'http'),
       
   139           'default': 'cookie',
       
   140           'help': 'authentication mode (cookie / http)',
       
   141           'group': 'web', 'level': 3,
       
   142           }),
       
   143         ('realm',
       
   144          {'type' : 'string',
       
   145           'default': 'cubicweb',
       
   146           'help': 'realm to use on HTTP authentication mode',
       
   147           'group': 'web', 'level': 3,
       
   148           }),
       
   149         ('http-session-time',
       
   150          {'type' : 'time',
       
   151           'default': 0,
       
   152           'help': "duration of the cookie used to store session identifier. "
       
   153           "If 0, the cookie will expire when the user exist its browser. "
       
   154           "Should be 0 or greater than repository\'s session-time.",
       
   155           'group': 'web', 'level': 2,
       
   156           }),
       
   157         ('cleanup-anonymous-session-time',
       
   158          {'type' : 'time',
       
   159           'default': '5min',
       
   160           'help': 'Same as cleanup-session-time but specific to anonymous '
       
   161           'sessions. You can have a much smaller timeout here since it will be '
       
   162           'transparent to the user. Default to 5min.',
       
   163           'group': 'web', 'level': 3,
       
   164           }),
       
   165         ('embed-allowed',
       
   166          {'type' : 'regexp',
       
   167           'default': None,
       
   168           'help': 'regular expression matching URLs that may be embeded. \
       
   169 leave it blank if you don\'t want the embedding feature, or set it to ".*" \
       
   170 if you want to allow everything',
       
   171           'group': 'web', 'level': 3,
       
   172           }),
       
   173         ('submit-mail',
       
   174          {'type' : 'string',
       
   175           'default': None,
       
   176           'help': ('Mail used as recipient to report bug in this instance, '
       
   177                    'if you want this feature on'),
       
   178           'group': 'web', 'level': 2,
       
   179           }),
       
   180 
       
   181         ('language-negociation',
       
   182          {'type' : 'yn',
       
   183           'default': True,
       
   184           'help': 'use Accept-Language http header to try to set user '\
       
   185           'interface\'s language according to browser defined preferences',
       
   186           'group': 'web', 'level': 2,
       
   187           }),
       
   188 
       
   189         ('print-traceback',
       
   190          {'type' : 'yn',
       
   191           'default': CubicWebConfiguration.mode != 'system',
       
   192           'help': 'print the traceback on the error page when an error occurred',
       
   193           'group': 'web', 'level': 2,
       
   194           }),
       
   195 
       
   196         ('captcha-font-file',
       
   197          {'type' : 'string',
       
   198           'default': join(CubicWebConfiguration.shared_dir(), 'data', 'porkys.ttf'),
       
   199           'help': 'True type font to use for captcha image generation (you \
       
   200 must have the python imaging library installed to use captcha)',
       
   201           'group': 'web', 'level': 3,
       
   202           }),
       
   203         ('captcha-font-size',
       
   204          {'type' : 'int',
       
   205           'default': 25,
       
   206           'help': 'Font size to use for captcha image generation (you must \
       
   207 have the python imaging library installed to use captcha)',
       
   208           'group': 'web', 'level': 3,
       
   209           }),
       
   210 
       
   211         ('concat-resources',
       
   212          {'type' : 'yn',
       
   213           'default': False,
       
   214           'help': 'use modconcat-like URLS to concat and serve JS / CSS files',
       
   215           'group': 'web', 'level': 2,
       
   216           }),
       
   217         ('anonymize-jsonp-queries',
       
   218          {'type': 'yn',
       
   219           'default': True,
       
   220           'help': 'anonymize the connection before executing any jsonp query.',
       
   221           'group': 'web', 'level': 1
       
   222           }),
       
   223         ('generate-staticdir',
       
   224          {'type': 'yn',
       
   225           'default': True,
       
   226           'help': 'Generate the static data resource directory on upgrade.',
       
   227           'group': 'web', 'level': 2,
       
   228           }),
       
   229         ('staticdir-path',
       
   230          {'type': 'string',
       
   231           'default': None,
       
   232           'help': 'The static data resource directory path.',
       
   233           'group': 'web', 'level': 2,
       
   234           }),
       
   235         ('access-control-allow-origin',
       
   236          {'type' : 'csv',
       
   237           'default': (),
       
   238           'help':('comma-separated list of allowed origin domains or "*" for any domain'),
       
   239           'group': 'web', 'level': 2,
       
   240           }),
       
   241         ('access-control-allow-methods',
       
   242          {'type' : 'csv',
       
   243           'default': (),
       
   244           'help': ('comma-separated list of allowed HTTP methods'),
       
   245           'group': 'web', 'level': 2,
       
   246           }),
       
   247         ('access-control-max-age',
       
   248          {'type' : 'int',
       
   249           'default': None,
       
   250           'help': ('maximum age of cross-origin resource sharing (in seconds)'),
       
   251           'group': 'web', 'level': 2,
       
   252           }),
       
   253         ('access-control-expose-headers',
       
   254          {'type' : 'csv',
       
   255           'default': (),
       
   256           'help':('comma-separated list of HTTP headers the application declare in response to a preflight request'),
       
   257           'group': 'web', 'level': 2,
       
   258           }),
       
   259         ('access-control-allow-headers',
       
   260          {'type' : 'csv',
       
   261           'default': (),
       
   262           'help':('comma-separated list of HTTP headers the application may set in the response'),
       
   263           'group': 'web', 'level': 2,
       
   264           }),
       
   265         ))
       
   266 
       
   267     def __init__(self, *args, **kwargs):
       
   268         super(WebConfiguration, self).__init__(*args, **kwargs)
       
   269         self.uiprops = None
       
   270         self.https_uiprops = None
       
   271         self.datadir_url = None
       
   272         self.https_datadir_url = None
       
   273 
       
   274     def fckeditor_installed(self):
       
   275         if self.uiprops is None:
       
   276             return False
       
   277         return exists(self.uiprops.get('FCKEDITOR_PATH', ''))
       
   278 
       
   279     def cwproperty_definitions(self):
       
   280         for key, pdef in super(WebConfiguration, self).cwproperty_definitions():
       
   281             if key == 'ui.fckeditor' and not self.fckeditor_installed():
       
   282                 continue
       
   283             yield key, pdef
       
   284 
       
   285     @deprecated('[3.22] call req.cnx.repo.get_versions() directly')
       
   286     def vc_config(self):
       
   287         return self.repository().get_versions()
       
   288 
       
   289     def anonymous_user(self):
       
   290         """return a login and password to use for anonymous users.
       
   291 
       
   292         None may be returned for both if anonymous connection is not
       
   293         allowed or if an empty login is used in configuration
       
   294         """
       
   295         try:
       
   296             user   = self['anonymous-user'] or None
       
   297             passwd = self['anonymous-password']
       
   298             if user:
       
   299                 user = text_type(user)
       
   300         except KeyError:
       
   301             user, passwd = None, None
       
   302         except UnicodeDecodeError:
       
   303             raise ConfigurationError("anonymous information should only contains ascii")
       
   304         return user, passwd
       
   305 
       
   306     @cachedproperty
       
   307     def _instance_salt(self):
       
   308         """This random key/salt is used to sign content to be sent back by
       
   309         browsers, eg. in the error report form.
       
   310         """
       
   311         return str(uuid4()).encode('ascii')
       
   312 
       
   313     def sign_text(self, text):
       
   314         """sign some text for later checking"""
       
   315         # hmac.new expect bytes
       
   316         if isinstance(text, text_type):
       
   317             text = text.encode('utf-8')
       
   318         # replace \r\n so we do not depend on whether a browser "reencode"
       
   319         # original message using \r\n or not
       
   320         return hmac.new(self._instance_salt,
       
   321                         text.strip().replace(b'\r\n', b'\n')).hexdigest()
       
   322 
       
   323     def check_text_sign(self, text, signature):
       
   324         """check the text signature is equal to the given signature"""
       
   325         return self.sign_text(text) == signature
       
   326 
       
   327     def locate_resource(self, rid):
       
   328         """return the (directory, filename) where the given resource
       
   329         may be found
       
   330         """
       
   331         return self._fs_locate(rid, 'data')
       
   332 
       
   333     def locate_doc_file(self, fname):
       
   334         """return the directory where the given resource may be found"""
       
   335         return self._fs_locate(fname, 'wdoc')[0]
       
   336 
       
   337     @cached
       
   338     def _fs_path_locate(self, rid, rdirectory):
       
   339         """return the directory where the given resource may be found"""
       
   340         path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
       
   341         for directory in path:
       
   342             if exists(join(directory, rdirectory, rid)):
       
   343                 return directory
       
   344 
       
   345     def _fs_locate(self, rid, rdirectory):
       
   346         """return the (directory, filename) where the given resource
       
   347         may be found
       
   348         """
       
   349         directory = self._fs_path_locate(rid, rdirectory)
       
   350         if directory is None:
       
   351             return None, None
       
   352         if rdirectory == 'data' and rid.endswith('.css'):
       
   353             if rid == 'cubicweb.old.css':
       
   354                 # @import('cubicweb.css') in css
       
   355                 warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
       
   356                      DeprecationWarning)
       
   357                 rid = 'cubicweb.css'
       
   358             return self.uiprops.process_resource(join(directory, rdirectory), rid), rid
       
   359         return join(directory, rdirectory), rid
       
   360 
       
   361     def locate_all_files(self, rid, rdirectory='wdoc'):
       
   362         """return all files corresponding to the given resource"""
       
   363         path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
       
   364         for directory in path:
       
   365             fpath = join(directory, rdirectory, rid)
       
   366             if exists(fpath):
       
   367                 yield join(fpath)
       
   368 
       
   369     def load_configuration(self, **kw):
       
   370         """load instance's configuration files"""
       
   371         super(WebConfiguration, self).load_configuration(**kw)
       
   372         # load external resources definition
       
   373         self._init_base_url()
       
   374         self._build_ui_properties()
       
   375 
       
   376     def _init_base_url(self):
       
   377         # normalize base url(s)
       
   378         baseurl = self['base-url'] or self.default_base_url()
       
   379         if baseurl and baseurl[-1] != '/':
       
   380             baseurl += '/'
       
   381         if not (self.repairing or self.creating):
       
   382             self.global_set_option('base-url', baseurl)
       
   383         self.datadir_url = self['datadir-url']
       
   384         if self.datadir_url:
       
   385             if self.datadir_url[-1] != '/':
       
   386                 self.datadir_url += '/'
       
   387             if self.mode != 'test':
       
   388                 self.datadir_url += '%s/' % self.instance_md5_version()
       
   389             self.https_datadir_url = self.datadir_url
       
   390             return
       
   391         httpsurl = self['https-url']
       
   392         data_relpath = self.data_relpath()
       
   393         if httpsurl:
       
   394             if httpsurl[-1] != '/':
       
   395                 httpsurl += '/'
       
   396                 if not self.repairing:
       
   397                     self.global_set_option('https-url', httpsurl)
       
   398             self.https_datadir_url = httpsurl + data_relpath
       
   399         self.datadir_url = baseurl + data_relpath
       
   400 
       
   401     def data_relpath(self):
       
   402         if self.mode == 'test':
       
   403             return 'data/'
       
   404         return 'data/%s/' % self.instance_md5_version()
       
   405 
       
   406     def _build_ui_properties(self):
       
   407         # self.datadir_url[:-1] to remove trailing /
       
   408         from cubicweb.web.propertysheet import PropertySheet
       
   409         cachedir = join(self.appdatahome, 'uicache')
       
   410         self.check_writeable_uid_directory(cachedir)
       
   411         self.uiprops = PropertySheet(
       
   412             cachedir,
       
   413             data=lambda x: self.datadir_url + x,
       
   414             datadir_url=self.datadir_url[:-1])
       
   415         self._init_uiprops(self.uiprops)
       
   416         if self['https-url']:
       
   417             cachedir = join(self.appdatahome, 'uicachehttps')
       
   418             self.check_writeable_uid_directory(cachedir)
       
   419             self.https_uiprops = PropertySheet(
       
   420                 cachedir,
       
   421                 data=lambda x: self.https_datadir_url + x,
       
   422                 datadir_url=self.https_datadir_url[:-1])
       
   423             self._init_uiprops(self.https_uiprops)
       
   424 
       
   425     def _init_uiprops(self, uiprops):
       
   426         libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
       
   427         uiprops.load(libuiprops)
       
   428         for path in reversed([self.apphome] + self.cubes_path()):
       
   429             self._load_ui_properties_file(uiprops, path)
       
   430         self._load_ui_properties_file(uiprops, self.apphome)
       
   431         datadir_url = uiprops.context['datadir_url']
       
   432         if (datadir_url+'/cubicweb.old.css') in uiprops['STYLESHEETS']:
       
   433             warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
       
   434                  DeprecationWarning)
       
   435             idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.old.css')
       
   436             uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.css'
       
   437         if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
       
   438             warn('[3.20] cubicweb.reset.css is obsolete', DeprecationWarning)
       
   439             uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
       
   440         cubicweb_js_url = datadir_url + '/cubicweb.js'
       
   441         if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
       
   442             uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
       
   443 
       
   444     def _load_ui_properties_file(self, uiprops, path):
       
   445         uipropsfile = join(path, 'uiprops.py')
       
   446         if exists(uipropsfile):
       
   447             self.debug('loading %s', uipropsfile)
       
   448             uiprops.load(uipropsfile)
       
   449 
       
   450     # static files handling ###################################################
       
   451 
       
   452     @property
       
   453     def static_directory(self):
       
   454         return join(self.appdatahome, 'static')
       
   455 
       
   456     def static_file_exists(self, rpath):
       
   457         return exists(join(self.static_directory, rpath))
       
   458 
       
   459     def static_file_open(self, rpath, mode='wb'):
       
   460         staticdir = self.static_directory
       
   461         rdir, filename = split(rpath)
       
   462         if rdir:
       
   463             staticdir = join(staticdir, rdir)
       
   464             if not isdir(staticdir) and 'w' in mode:
       
   465                 os.makedirs(staticdir)
       
   466         return open(join(staticdir, filename), mode)
       
   467 
       
   468     def static_file_add(self, rpath, data):
       
   469         stream = self.static_file_open(rpath)
       
   470         stream.write(data)
       
   471         stream.close()
       
   472 
       
   473     def static_file_del(self, rpath):
       
   474         if self.static_file_exists(rpath):
       
   475             os.remove(join(self.static_directory, rpath))