Source code for cubicweb_web.webconfig

# copyright 2003-2024 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://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 <https://www.gnu.org/licenses/>.
"""web ui configuration for cubicweb instances"""


import hmac
import os
from os.path import dirname, join, exists, split, isdir
from uuid import uuid4

from logilab.common.configuration import Method, merge_options
from logilab.common.decorators import cached, cachedproperty

from cubicweb import _
from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options
from cubicweb.pyramid.config import AllInOneConfiguration

_DATA_DIR = join(dirname(__file__), "data")


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):
    """web instance (in a web server) client of a RQL server"""

    cube_appobject_path = CubicWebConfiguration.cube_appobject_path | {"views"}

    options = merge_options(
        CubicWebConfiguration.options
        + (
            (
                "use-uicache",
                {
                    "type": "yn",
                    "default": True,
                    "help": _("should css be compiled and store in uicache"),
                    "group": "ui",
                    "level": 2,
                },
            ),
            (
                "query-log-file",
                {
                    "type": "string",
                    "default": None,
                    "help": "web instance query log file",
                    "group": "web",
                    "level": 3,
                },
            ),
            # web configuration
            (
                "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,
                },
            ),
            (
                "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-mode",
                {
                    "type": "choice",
                    "choices": ("http-negotiation", "url-prefix", ""),
                    "default": "http-negotiation",
                    "help": (
                        "source for interface's language detection. "
                        'If set to "http-negotiation" the Accept-Language HTTP header will be used,'
                        ' if set to "url-prefix", the URL will be inspected for a'
                        " short language prefix."
                    ),
                    "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(_DATA_DIR, "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": False,
                    "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,
                },
            ),
            # ctl configuration
            (
                "max-post-length",  # XXX specific to "wsgi" server
                {
                    "type": "bytes",
                    "default": "100MB",
                    "help": "maximum length of HTTP request. Default to 100 MB.",
                    "group": "web",
                    "level": 1,
                },
            ),
            (
                "pid-file",
                {
                    "type": "string",
                    "default": Method("default_pid_file"),
                    "help": "repository's pid file",
                    "group": "main",
                    "level": 2,
                },
            ),
        )
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.uiprops = None
        self.datadir_url = None
        self._debug = None
        # don't register ReStructured Text directives by simple import, avoid pb
        # with eg sphinx.
        # XXX should be done properly with a function from cw.uicfg
        try:
            from cubicweb_web.ext.rest import cw_rest_init
        except ImportError:
            pass
        else:
            cw_rest_init()

    @property
    def in_debug_mode(self):
        """
        we need all of this because self["debug"] is regularely overwritten in
        the workflow because the configuration is not initialy loaded and
        loaded after most includeme.
        """
        if self._debug is not None:
            return self._debug

        return self["debug"]

    @in_debug_mode.setter
    def in_debug_mode(self, value):
        self._debug = value

    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().cwproperty_definitions():
            if key == "ui.fckeditor" and not self.fckeditor_installed():
                continue
            yield key, pdef

    @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, str):
            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"),
            digestmod="sha3_512",
        ).hexdigest()

    def check_text_sign(self, text, signature):
        """check the text signature is equal to the given signature"""
        return self.sign_text(text) == signature

[docs] def locate_resource(self, rid): """return the (directory, filename) where the given resource may be found """ return self._fs_locate(rid, "data")
[docs] 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() + [dirname(__file__)] 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 self["use-uicache"] and rdirectory == "data" and rid.endswith(".css"): return ( self.ensure_uid_directory( self.uiprops.process_resource(join(directory, rdirectory), rid) ), rid, ) return join(directory, rdirectory), rid
[docs] def locate_all_files(self, rid, rdirectory="wdoc"): """return all files corresponding to the given resource""" path = [self.apphome] + self.cubes_path() + [dirname(__file__)] 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().load_configuration(**kw) # load external resources definition self._build_ui_properties() def _init_base_url(self): super()._init_base_url() 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() return data_relpath = self.data_relpath() self.datadir_url = self._generate_base_url() + 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) def _init_uiprops(self, uiprops): libuiprops = join(_DATA_DIR, "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"] 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")
[docs] def static_file_exists(self, rpath): return exists(join(self.static_directory, rpath))
[docs] 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: self.check_writeable_uid_directory(staticdir) return open(join(staticdir, filename), mode)
[docs] def static_file_add(self, rpath, data): stream = self.static_file_open(rpath) stream.write(data) stream.close() self.ensure_uid(rpath)
[docs] def static_file_del(self, rpath): if self.static_file_exists(rpath): os.remove(join(self.static_directory, rpath))
class WebAllInOneConfiguration(AllInOneConfiguration, WebConfiguration): """web instance (in a web server) client of a RQL server""" name = "all-in-one" options = merge_options(WebConfiguration.options + AllInOneConfiguration.options) cubicweb_appobject_path = ( WebConfiguration.cubicweb_appobject_path | AllInOneConfiguration.cubicweb_appobject_path ) cube_appobject_path = ( WebConfiguration.cube_appobject_path | AllInOneConfiguration.cube_appobject_path )