230 строки
7.4 KiB
Python
230 строки
7.4 KiB
Python
#-*- coding: utf-8 -*-
|
|
import hashlib
|
|
import hmac
|
|
import urllib
|
|
from threading import local
|
|
from urlparse import urlparse, urlsplit, urlunsplit
|
|
|
|
from django.conf import settings
|
|
from django.core import urlresolvers
|
|
from django.utils import encoding
|
|
from django.utils.translation.trans_real import parse_accept_lang_header
|
|
|
|
import jinja2
|
|
|
|
import amo
|
|
|
|
# Get a pointer to Django's reverse because we're going to hijack it after we
|
|
# define our own.
|
|
django_reverse = urlresolvers.reverse
|
|
|
|
|
|
# Thread-local storage for URL prefixes. Access with {get,set}_url_prefix.
|
|
_local = local()
|
|
|
|
|
|
def set_url_prefix(prefix):
|
|
"""Set ``prefix`` for the current thread."""
|
|
_local.prefix = prefix
|
|
|
|
|
|
def get_url_prefix():
|
|
"""Get the prefix for the current thread, or None."""
|
|
return getattr(_local, 'prefix', None)
|
|
|
|
|
|
def clean_url_prefixes():
|
|
"""Purge prefix cache."""
|
|
if hasattr(_local, 'prefix'):
|
|
delattr(_local, 'prefix')
|
|
|
|
|
|
def get_app_redirect(app):
|
|
"""Redirect request to another app."""
|
|
prefixer = get_url_prefix()
|
|
old_app = prefixer.app
|
|
prefixer.app = app.short
|
|
(_, _, url) = prefixer.split_path(prefixer.request.get_full_path())
|
|
new_url = prefixer.fix(url)
|
|
prefixer.app = old_app
|
|
return new_url
|
|
|
|
|
|
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None,
|
|
current_app=None, add_prefix=True):
|
|
"""Wraps django's reverse to prepend the correct locale and app."""
|
|
prefixer = get_url_prefix()
|
|
if settings.MARKETPLACE and settings.REGION_STORES:
|
|
prefix = None
|
|
# Blank out the script prefix since we add that in prefixer.fix().
|
|
if prefixer:
|
|
prefix = prefix or '/'
|
|
url = django_reverse(viewname, urlconf, args, kwargs, prefix, current_app)
|
|
if prefixer and add_prefix:
|
|
return prefixer.fix(url)
|
|
else:
|
|
return url
|
|
|
|
# Replace Django's reverse with our own.
|
|
urlresolvers.reverse = reverse
|
|
|
|
|
|
class Prefixer(object):
|
|
|
|
def __init__(self, request):
|
|
self.request = request
|
|
split = self.split_path(request.path_info)
|
|
self.locale, self.app, self.shortened_path = split
|
|
|
|
def split_path(self, path_):
|
|
"""
|
|
Split the requested path into (locale, app, remainder).
|
|
|
|
locale and app will be empty strings if they're not found.
|
|
"""
|
|
path = path_.lstrip('/')
|
|
|
|
# Use partition instead of split since it always returns 3 parts.
|
|
first, _, first_rest = path.partition('/')
|
|
second, _, rest = first_rest.partition('/')
|
|
|
|
first_lower = first.lower()
|
|
lang, dash, territory = first_lower.partition('-')
|
|
|
|
# Check language-territory first.
|
|
if first_lower in settings.LANGUAGES:
|
|
if second in amo.APPS:
|
|
return first, second, rest
|
|
else:
|
|
return first, '', first_rest
|
|
# And check just language next.
|
|
elif dash and lang in settings.LANGUAGES:
|
|
first = lang
|
|
if second in amo.APPS:
|
|
return first, second, rest
|
|
else:
|
|
return first, '', first_rest
|
|
elif first in amo.APPS:
|
|
return '', first, first_rest
|
|
else:
|
|
if second in amo.APPS:
|
|
return '', second, rest
|
|
else:
|
|
return '', '', path
|
|
|
|
def get_app(self):
|
|
"""
|
|
Return a valid application string using the User Agent to guess. Falls
|
|
back to settings.DEFAULT_APP.
|
|
"""
|
|
ua = self.request.META.get('HTTP_USER_AGENT')
|
|
if ua:
|
|
for app in amo.APP_DETECT:
|
|
if app.matches_user_agent(ua):
|
|
return app.short
|
|
|
|
return settings.DEFAULT_APP
|
|
|
|
def get_language(self):
|
|
"""
|
|
Return a locale code that we support on the site using the
|
|
user's Accept Language header to determine which is best. This
|
|
mostly follows the RFCs but read bug 439568 for details.
|
|
"""
|
|
data = (self.request.GET or self.request.POST)
|
|
if 'lang' in data:
|
|
lang = data['lang'].lower()
|
|
if lang in settings.LANGUAGE_URL_MAP:
|
|
return settings.LANGUAGE_URL_MAP[lang]
|
|
prefix = lang.split('-')[0]
|
|
if prefix in settings.LANGUAGE_URL_MAP:
|
|
return settings.LANGUAGE_URL_MAP[prefix]
|
|
|
|
accept = self.request.META.get('HTTP_ACCEPT_LANGUAGE', '')
|
|
return lang_from_accept_header(accept)
|
|
|
|
def fix(self, path):
|
|
# Marketplace URLs are not prefixed with `/<locale>/<app>`.
|
|
if settings.MARKETPLACE and settings.REGION_STORES:
|
|
return path
|
|
|
|
path = path.lstrip('/')
|
|
url_parts = [self.request.META['SCRIPT_NAME']]
|
|
|
|
if path.partition('/')[0] not in settings.SUPPORTED_NONLOCALES:
|
|
url_parts.append(self.locale or self.get_language())
|
|
|
|
if (not settings.MARKETPLACE and
|
|
path.partition('/')[0] not in settings.SUPPORTED_NONAPPS):
|
|
url_parts.append(self.app or self.get_app())
|
|
|
|
url_parts.append(path)
|
|
|
|
return '/'.join(url_parts)
|
|
|
|
|
|
def get_outgoing_url(url):
|
|
"""
|
|
Bounce a URL off an outgoing URL redirector, such as outgoing.mozilla.org.
|
|
"""
|
|
if not settings.REDIRECT_URL:
|
|
return url
|
|
|
|
url_netloc = urlparse(url).netloc
|
|
|
|
# No double-escaping, and some domain names are excluded.
|
|
if (url_netloc == urlparse(settings.REDIRECT_URL).netloc
|
|
or url_netloc in settings.REDIRECT_URL_WHITELIST):
|
|
return url
|
|
|
|
url = encoding.smart_str(jinja2.utils.Markup(url).unescape())
|
|
sig = hmac.new(settings.REDIRECT_SECRET_KEY,
|
|
msg=url, digestmod=hashlib.sha256).hexdigest()
|
|
# Let '&=' through so query params aren't escaped. We probably shouldn't
|
|
# bother to quote the query part at all.
|
|
return '/'.join([settings.REDIRECT_URL.rstrip('/'), sig,
|
|
urllib.quote(url, safe='/&=')])
|
|
|
|
|
|
def url_fix(s, charset='utf-8'):
|
|
"""Sometimes you get an URL by a user that just isn't a real
|
|
URL because it contains unsafe characters like ' ' and so on. This
|
|
function can fix some of the problems in a similar way browsers
|
|
handle data entered by the user:
|
|
|
|
>>> url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)')
|
|
'http://de.wikipedia.org/wiki/Elf%20%28Begriffskl%C3%A4rung%29'
|
|
|
|
:param charset: The target charset for the URL if the url was
|
|
given as unicode string.
|
|
|
|
Lifted from Werkzeug.
|
|
"""
|
|
if isinstance(s, unicode):
|
|
s = s.encode(charset, 'ignore')
|
|
scheme, netloc, path, qs, anchor = urlsplit(s)
|
|
path = urllib.quote(path, '/%:')
|
|
qs = urllib.quote_plus(qs, ':&=')
|
|
return urlunsplit((scheme, netloc, path, qs, anchor))
|
|
|
|
|
|
def lang_from_accept_header(header):
|
|
# Map all our lang codes and any prefixes to the locale code.
|
|
langs = dict((k.lower(), v) for k, v in settings.LANGUAGE_URL_MAP.items())
|
|
|
|
# If we have a lang or a prefix of the lang, return the locale code.
|
|
for lang, _ in parse_accept_lang_header(header.lower()):
|
|
if lang in langs:
|
|
return langs[lang]
|
|
|
|
prefix = lang.split('-')[0]
|
|
# Downgrade a longer prefix to a shorter one if needed (es-PE > es)
|
|
if prefix in langs:
|
|
return langs[prefix]
|
|
# Upgrade to a longer one, if present (zh > zh-CN)
|
|
lookup = settings.SHORTER_LANGUAGES.get(prefix, '').lower()
|
|
if lookup and lookup in langs:
|
|
return langs[lookup]
|
|
|
|
return settings.LANGUAGE_CODE
|