summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/controllers/home.py
blob: 403d4de70e62d24ef8d85c6b342bb3e853e92ca5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# -*- coding: utf-8 -*-
from __future__ import absolute_import

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<locale>[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*|\*)(;q=(?P<weight>[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')