import json
import os
import pkgutil
+import re
import sys
import six
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,
mapper.connect(name, url, controller=inst, action=endpoint.action,
conditions=conditions)
- return endp_base_url
+ return parent_url
def generate_routes(url_prefix):
@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'])
--- /dev/null
+# -*- 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<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[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)