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