# -*- coding: utf-8 -*- import json import logging import os import re try: from functools import lru_cache except ImportError: from ..plugins.lru_cache import lru_cache import cherrypy from cherrypy.lib.static import serve_file from .. import mgr from . import BaseController, Endpoint, Proxy, Router, UIRouter logger = logging.getLogger("controllers.home") class LanguageMixin(object): def __init__(self): try: self.LANGUAGES = { f for f in os.listdir(mgr.get_frontend_path()) if os.path.isdir(os.path.join(mgr.get_frontend_path(), f)) } except FileNotFoundError: logger.exception("Build directory missing") self.LANGUAGES = {} self.LANGUAGES_PATH_MAP = { f.lower(): { 'lang': f, 'path': os.path.join(mgr.get_frontend_path(), f) } for f in self.LANGUAGES } # pre-populating with the primary language subtag. for lang in list(self.LANGUAGES_PATH_MAP.keys()): if '-' in lang: self.LANGUAGES_PATH_MAP[lang.split('-')[0]] = { 'lang': self.LANGUAGES_PATH_MAP[lang]['lang'], 'path': self.LANGUAGES_PATH_MAP[lang]['path'] } with open(os.path.normpath("{}/../package.json".format(mgr.get_frontend_path())), "r") as f: config = json.load(f) self.DEFAULT_LANGUAGE = config['config']['locale'] self.DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(), self.DEFAULT_LANGUAGE) super().__init__() @Router("/", secure=False) class HomeController(BaseController, LanguageMixin): LANG_TAG_SEQ_RE = re.compile(r'\s*([^,]+)\s*,?\s*') LANG_TAG_RE = re.compile( r'^(?P[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*|\*)(;q=(?P[01]\.\d{0,3}))?$') MAX_ACCEPTED_LANGS = 10 @lru_cache() def _parse_accept_language(self, accept_lang_header): result = [] for i, m in enumerate(self.LANG_TAG_SEQ_RE.finditer(accept_lang_header)): if i >= self.MAX_ACCEPTED_LANGS: logger.debug("reached max accepted languages, skipping remaining") break tag_match = self.LANG_TAG_RE.match(m.group(1)) if tag_match is None: raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header") locale = tag_match.group('locale').lower() weight = tag_match.group('weight') if weight: try: ratio = float(weight) except ValueError: raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header") else: ratio = 1.0 result.append((locale, ratio)) result.sort(key=lambda l: l[0]) result.sort(key=lambda l: l[1], reverse=True) logger.debug("language preference: %s", result) return [r[0] for r in result] def _language_dir(self, langs): for lang in langs: if lang in self.LANGUAGES_PATH_MAP: logger.debug("found directory for language '%s'", lang) cherrypy.response.headers[ 'Content-Language'] = self.LANGUAGES_PATH_MAP[lang]['lang'] return self.LANGUAGES_PATH_MAP[lang]['path'] logger.debug("Languages '%s' not available, falling back to %s", langs, self.DEFAULT_LANGUAGE) cherrypy.response.headers['Content-Language'] = self.DEFAULT_LANGUAGE return self.DEFAULT_LANGUAGE_PATH @Proxy() def __call__(self, path, **params): if not path: path = "index.html" if 'cd-lang' in cherrypy.request.cookie: langs = [cherrypy.request.cookie['cd-lang'].value.lower()] logger.debug("frontend language from cookie: %s", langs) else: if 'Accept-Language' in cherrypy.request.headers: accept_lang_header = cherrypy.request.headers['Accept-Language'] langs = self._parse_accept_language(accept_lang_header) else: langs = [self.DEFAULT_LANGUAGE.lower()] logger.debug("frontend language from headers: %s", langs) base_dir = self._language_dir(langs) full_path = os.path.join(base_dir, path) # Block uplevel attacks if not os.path.normpath(full_path).startswith(os.path.normpath(base_dir)): raise cherrypy.HTTPError(403) # Forbidden logger.debug("serving static content: %s", full_path) if 'Vary' in cherrypy.response.headers: cherrypy.response.headers['Vary'] = "{}, Accept-Language" else: cherrypy.response.headers['Vary'] = "Accept-Language" cherrypy.response.headers['Cache-control'] = "no-cache" return serve_file(full_path) @UIRouter("/langs", secure=False) class LangsController(BaseController, LanguageMixin): @Endpoint('GET') def __call__(self): return list(self.LANGUAGES) @UIRouter("/login", secure=False) class LoginController(BaseController): @Endpoint('GET', 'custom_banner') def __call__(self): return mgr.get_store('custom_login_banner')