]>
Commit | Line | Data |
---|---|---|
eafe8130 | 1 | # -*- coding: utf-8 -*- |
eafe8130 | 2 | |
eafe8130 | 3 | import json |
9f95a23c | 4 | import logging |
f67539c2 TL |
5 | import os |
6 | import re | |
7 | ||
eafe8130 TL |
8 | try: |
9 | from functools import lru_cache | |
10 | except ImportError: | |
11 | from ..plugins.lru_cache import lru_cache | |
12 | ||
13 | import cherrypy | |
14 | from cherrypy.lib.static import serve_file | |
15 | ||
9f95a23c | 16 | from .. import mgr |
a4b75251 | 17 | from . import BaseController, Endpoint, Proxy, Router, UIRouter |
9f95a23c TL |
18 | |
19 | logger = logging.getLogger("controllers.home") | |
20 | ||
21 | ||
22 | class LanguageMixin(object): | |
23 | def __init__(self): | |
f67539c2 TL |
24 | try: |
25 | self.LANGUAGES = { | |
26 | f | |
27 | for f in os.listdir(mgr.get_frontend_path()) | |
28 | if os.path.isdir(os.path.join(mgr.get_frontend_path(), f)) | |
29 | } | |
30 | except FileNotFoundError: | |
31 | logger.exception("Build directory missing") | |
32 | self.LANGUAGES = {} | |
33 | ||
9f95a23c TL |
34 | self.LANGUAGES_PATH_MAP = { |
35 | f.lower(): { | |
36 | 'lang': f, | |
37 | 'path': os.path.join(mgr.get_frontend_path(), f) | |
38 | } | |
39 | for f in self.LANGUAGES | |
40 | } | |
41 | # pre-populating with the primary language subtag. | |
42 | for lang in list(self.LANGUAGES_PATH_MAP.keys()): | |
43 | if '-' in lang: | |
44 | self.LANGUAGES_PATH_MAP[lang.split('-')[0]] = { | |
45 | 'lang': self.LANGUAGES_PATH_MAP[lang]['lang'], | |
46 | 'path': self.LANGUAGES_PATH_MAP[lang]['path'] | |
47 | } | |
f67539c2 | 48 | with open(os.path.normpath("{}/../package.json".format(mgr.get_frontend_path())), |
9f95a23c TL |
49 | "r") as f: |
50 | config = json.load(f) | |
51 | self.DEFAULT_LANGUAGE = config['config']['locale'] | |
52 | self.DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(), | |
53 | self.DEFAULT_LANGUAGE) | |
a4b75251 | 54 | super().__init__() |
eafe8130 TL |
55 | |
56 | ||
a4b75251 | 57 | @Router("/", secure=False) |
9f95a23c | 58 | class HomeController(BaseController, LanguageMixin): |
eafe8130 TL |
59 | LANG_TAG_SEQ_RE = re.compile(r'\s*([^,]+)\s*,?\s*') |
60 | LANG_TAG_RE = re.compile( | |
522d829b | 61 | r'^(?P<locale>[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*|\*)(;q=(?P<weight>[01]\.\d{0,3}))?$') |
eafe8130 TL |
62 | MAX_ACCEPTED_LANGS = 10 |
63 | ||
64 | @lru_cache() | |
65 | def _parse_accept_language(self, accept_lang_header): | |
66 | result = [] | |
67 | for i, m in enumerate(self.LANG_TAG_SEQ_RE.finditer(accept_lang_header)): | |
68 | if i >= self.MAX_ACCEPTED_LANGS: | |
69 | logger.debug("reached max accepted languages, skipping remaining") | |
70 | break | |
71 | ||
72 | tag_match = self.LANG_TAG_RE.match(m.group(1)) | |
73 | if tag_match is None: | |
74 | raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header") | |
75 | locale = tag_match.group('locale').lower() | |
76 | weight = tag_match.group('weight') | |
77 | if weight: | |
78 | try: | |
79 | ratio = float(weight) | |
80 | except ValueError: | |
81 | raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header") | |
82 | else: | |
83 | ratio = 1.0 | |
84 | result.append((locale, ratio)) | |
85 | ||
86 | result.sort(key=lambda l: l[0]) | |
87 | result.sort(key=lambda l: l[1], reverse=True) | |
88 | logger.debug("language preference: %s", result) | |
f67539c2 | 89 | return [r[0] for r in result] |
eafe8130 TL |
90 | |
91 | def _language_dir(self, langs): | |
92 | for lang in langs: | |
9f95a23c | 93 | if lang in self.LANGUAGES_PATH_MAP: |
eafe8130 | 94 | logger.debug("found directory for language '%s'", lang) |
9f95a23c TL |
95 | cherrypy.response.headers[ |
96 | 'Content-Language'] = self.LANGUAGES_PATH_MAP[lang]['lang'] | |
97 | return self.LANGUAGES_PATH_MAP[lang]['path'] | |
eafe8130 | 98 | |
9f95a23c TL |
99 | logger.debug("Languages '%s' not available, falling back to %s", |
100 | langs, self.DEFAULT_LANGUAGE) | |
101 | cherrypy.response.headers['Content-Language'] = self.DEFAULT_LANGUAGE | |
102 | return self.DEFAULT_LANGUAGE_PATH | |
eafe8130 TL |
103 | |
104 | @Proxy() | |
105 | def __call__(self, path, **params): | |
106 | if not path: | |
107 | path = "index.html" | |
108 | ||
109 | if 'cd-lang' in cherrypy.request.cookie: | |
110 | langs = [cherrypy.request.cookie['cd-lang'].value.lower()] | |
111 | logger.debug("frontend language from cookie: %s", langs) | |
112 | else: | |
113 | if 'Accept-Language' in cherrypy.request.headers: | |
114 | accept_lang_header = cherrypy.request.headers['Accept-Language'] | |
115 | langs = self._parse_accept_language(accept_lang_header) | |
116 | else: | |
9f95a23c | 117 | langs = [self.DEFAULT_LANGUAGE.lower()] |
eafe8130 TL |
118 | logger.debug("frontend language from headers: %s", langs) |
119 | ||
120 | base_dir = self._language_dir(langs) | |
121 | full_path = os.path.join(base_dir, path) | |
92f5a8d4 TL |
122 | |
123 | # Block uplevel attacks | |
124 | if not os.path.normpath(full_path).startswith(os.path.normpath(base_dir)): | |
125 | raise cherrypy.HTTPError(403) # Forbidden | |
126 | ||
eafe8130 TL |
127 | logger.debug("serving static content: %s", full_path) |
128 | if 'Vary' in cherrypy.response.headers: | |
129 | cherrypy.response.headers['Vary'] = "{}, Accept-Language" | |
130 | else: | |
131 | cherrypy.response.headers['Vary'] = "Accept-Language" | |
9f95a23c TL |
132 | |
133 | cherrypy.response.headers['Cache-control'] = "no-cache" | |
eafe8130 TL |
134 | return serve_file(full_path) |
135 | ||
136 | ||
a4b75251 | 137 | @UIRouter("/langs", secure=False) |
9f95a23c | 138 | class LangsController(BaseController, LanguageMixin): |
eafe8130 TL |
139 | @Endpoint('GET') |
140 | def __call__(self): | |
9f95a23c | 141 | return list(self.LANGUAGES) |
33c7a0ef TL |
142 | |
143 | ||
144 | @UIRouter("/login", secure=False) | |
145 | class LoginController(BaseController): | |
146 | @Endpoint('GET', 'custom_banner') | |
147 | def __call__(self): | |
2a845540 | 148 | return mgr.get_store('custom_login_banner') |