From: Ricardo Dias Date: Mon, 30 Sep 2019 14:53:41 +0000 (+0100) Subject: dashboard: detect language and serve correct frontend app X-Git-Tag: v15.1.0~1273^2~11 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=1bdb87f6c01dadec7f7897161ffde3b0037404ed;p=ceph.git dashboard: detect language and serve correct frontend app Signed-off-by: Ricardo Dias --- diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py index 3c0050e8ccff7..8c825305400bb 100644 --- a/src/pybind/mgr/dashboard/__init__.py +++ b/src/pybind/mgr/dashboard/__init__.py @@ -56,3 +56,4 @@ else: sys.modules['ceph_module'] = mock.Mock() mgr = mock.Mock() + mgr.get_frontend_path.side_effect = lambda: "./frontend/dist" diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index fbbf7252f4ec0..04169b3997331 100644 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -8,6 +8,7 @@ import inspect import json import os import pkgutil +import re import sys import six @@ -292,24 +293,32 @@ ENDPOINT_MAP = collections.defaultdict(list) def generate_controller_routes(endpoint, mapper, base_url): inst = endpoint.inst ctrl_class = endpoint.ctrl - endp_base_url = None if endpoint.proxy: conditions = None else: conditions = dict(method=[endpoint.method]) + # base_url can be empty or a URL path that starts with "/" + # we will remove the trailing "/" if exists to help with the + # concatenation with the endpoint url below + if base_url.endswith("/"): + base_url = base_url[:-1] + endp_url = endpoint.url - if base_url == "/": - base_url = "" - if endp_url == "/" and base_url: - endp_url = "" - url = "{}{}".format(base_url, endp_url) - if '/' in url[len(base_url)+1:]: - endp_base_url = url[:len(base_url)+1+endp_url[1:].find('/')] + if endp_url.find("/", 1) == -1: + parent_url = "{}{}".format(base_url, endp_url) else: - endp_base_url = url + parent_url = "{}{}".format(base_url, endp_url[:endp_url.find("/", 1)]) + + # parent_url might be of the form "/.../{...}" where "{...}" is a path parameter + # we need to remove the path parameter definition + parent_url = re.sub(r'(?:/\{[^}]+\})$', '', parent_url) + if not parent_url: # root path case + parent_url = "/" + + url = "{}{}".format(base_url, endp_url) logger.debug("Mapped [%s] to %s:%s restricted to %s", url, ctrl_class.__name__, endpoint.action, @@ -327,7 +336,7 @@ def generate_controller_routes(endpoint, mapper, base_url): mapper.connect(name, url, controller=inst, action=endpoint.action, conditions=conditions) - return endp_base_url + return parent_url def generate_routes(url_prefix): @@ -500,10 +509,13 @@ class BaseController(object): @property def url(self): + ctrl_path = self.ctrl.get_path() + if ctrl_path == "/": + ctrl_path = "" if self.config['path'] is not None: - url = "{}{}".format(self.ctrl.get_path(), self.config['path']) + url = "{}{}".format(ctrl_path, self.config['path']) else: - url = "{}/{}".format(self.ctrl.get_path(), self.func.__name__) + url = "{}/{}".format(ctrl_path, self.func.__name__) ctrl_path_params = self.ctrl.get_path_param_names( self.config['path']) diff --git a/src/pybind/mgr/dashboard/controllers/home.py b/src/pybind/mgr/dashboard/controllers/home.py new file mode 100644 index 0000000000000..32613f989bca0 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/home.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import os +import re +import json +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 Controller, BaseController, Proxy +from .. import mgr, logger + + +LANGUAGES = {f for f in os.listdir(mgr.get_frontend_path()) + if os.path.isdir(os.path.join(mgr.get_frontend_path(), f))} +LANGUAGES_PATH_MAP = {f.lower(): {'lang': f, 'path': os.path.join(mgr.get_frontend_path(), f)} + for f in LANGUAGES} +# pre-populating with the primary language subtag +for _lang in list(LANGUAGES_PATH_MAP.keys()): + if '-' in _lang: + LANGUAGES_PATH_MAP[_lang.split('-')[0]] = { + 'lang': LANGUAGES_PATH_MAP[_lang]['lang'], 'path': LANGUAGES_PATH_MAP[_lang]['path']} + + +def _get_default_language(): + with open("{}/../package.json".format(mgr.get_frontend_path()), "r") as f: + config = json.load(f) + return config['config']['locale'] + + +DEFAULT_LANGUAGE = _get_default_language() +DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(), DEFAULT_LANGUAGE) + + +@Controller("/", secure=False) +class HomeController(BaseController): + 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[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 [l[0] for l in result] + + def _language_dir(self, langs): + for lang in langs: + if lang in LANGUAGES_PATH_MAP: + logger.debug("found directory for language '%s'", lang) + cherrypy.response.headers['Content-Language'] = LANGUAGES_PATH_MAP[lang]['lang'] + return LANGUAGES_PATH_MAP[lang]['path'] + + logger.debug("Languages '%s' not available, falling back to %s", langs, DEFAULT_LANGUAGE) + cherrypy.response.headers['Content-Language'] = DEFAULT_LANGUAGE + return 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 = [DEFAULT_LANGUAGE] + logger.debug("frontend language from headers: %s", langs) + + base_dir = self._language_dir(langs) + full_path = os.path.join(base_dir, path) + 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" + return serve_file(full_path) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/locale.helper.ts b/src/pybind/mgr/dashboard/frontend/src/app/locale.helper.ts index 21fb857050ddc..5c5b9d2209b56 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/locale.helper.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/locale.helper.ts @@ -52,6 +52,7 @@ export class LocaleHelper { } static setLocale(lang: string) { + document.cookie = `cd-lang=${lang}`; window.localStorage.setItem('lang', lang); } diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index dcfa42fcaa9e1..7cf4605e16f38 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -327,13 +327,7 @@ class Module(MgrModule, CherryPyConfig): mapper, parent_urls = generate_routes(self.url_prefix) - config = { - self.url_prefix or '/': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': self.get_frontend_path(), - 'tools.staticdir.index': 'index.html' - } - } + config = {} for purl in parent_urls: config[purl] = { 'request.dispatch': mapper