addons-server/apps/amo/middleware.py

250 строки
8.8 KiB
Python

"""
Borrowed from: http://code.google.com/p/django-localeurl
Note: didn't make sense to use localeurl since we need to capture app as well
"""
import contextlib
import time
import urllib
from django.conf import settings
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import Http404, HttpResponsePermanentRedirect
from django.middleware import common
from django.shortcuts import redirect
from django.utils.cache import patch_vary_headers, patch_cache_control
from django.utils.encoding import iri_to_uri, smart_str
import commonware.log
import lxml.html
import MySQLdb as mysql
import tower
import jingo
from statsd import statsd
import amo
from . import urlresolvers
from .helpers import urlparams
class LocaleAndAppURLMiddleware(object):
"""
1. search for locale first
2. see if there are acceptable apps
3. save those matched parameters in the request
4. strip them from the URL so we can do stuff
"""
def process_request(self, request):
# Find locale, app
prefixer = urlresolvers.Prefixer(request)
urlresolvers.set_url_prefix(prefixer)
full_path = prefixer.fix(prefixer.shortened_path)
if 'lang' in request.GET:
# Blank out the locale so that we can set a new one. Remove lang
# from query params so we don't have an infinite loop.
prefixer.locale = ''
new_path = prefixer.fix(prefixer.shortened_path)
query = dict((smart_str(k), request.GET[k]) for k in request.GET)
query.pop('lang')
return HttpResponsePermanentRedirect(urlparams(new_path, **query))
if full_path != request.path:
query_string = request.META.get('QUERY_STRING', '')
full_path = urllib.quote(full_path.encode('utf-8'))
if query_string:
full_path = "%s?%s" % (full_path, query_string)
response = HttpResponsePermanentRedirect(full_path)
# Cache the redirect for a year.
if not settings.DEBUG:
patch_cache_control(response, max_age=60 * 60 * 24 * 365)
# Vary on Accept-Language or User-Agent if we changed the locale or
# app.
old_app = prefixer.app
old_locale = prefixer.locale
new_locale, new_app, _ = prefixer.split_path(full_path)
if old_locale != new_locale:
patch_vary_headers(response, ['Accept-Language'])
if old_app != new_app:
patch_vary_headers(response, ['User-Agent'])
return response
request.path_info = '/' + prefixer.shortened_path
tower.activate(prefixer.locale)
request.APP = amo.APPS.get(prefixer.app, amo.FIREFOX)
request.LANG = prefixer.locale
class NoVarySessionMiddleware(SessionMiddleware):
"""
SessionMiddleware sets Vary: Cookie anytime request.session is accessed.
request.session is accessed indirectly anytime request.user is touched.
We always touch request.user to see if the user is authenticated, so every
request would be sending vary, so we'd get no caching.
We skip the cache in Zeus if someone has an AMOv3 cookie, so varying on
Cookie at this level only hurts us.
"""
def process_response(self, request, response):
if settings.READ_ONLY:
return response
# Let SessionMiddleware do its processing but prevent it from changing
# the Vary header.
vary = response.get('Vary', None)
new_response = (super(NoVarySessionMiddleware, self)
.process_response(request, response))
if vary:
new_response['Vary'] = vary
else:
del new_response['Vary']
return new_response
class RemoveSlashMiddleware(object):
"""
Middleware that tries to remove a trailing slash if there was a 404.
If the response is a 404 because url resolution failed, we'll look for a
better url without a trailing slash.
"""
def process_response(self, request, response):
if (response.status_code == 404
and request.path_info.endswith('/')
and not common._is_valid_path(request.path_info)
and common._is_valid_path(request.path_info[:-1])):
# Use request.path because we munged app/locale in path_info.
newurl = request.path[:-1]
if request.GET:
with safe_query_string(request):
newurl += '?' + request.META['QUERY_STRING']
return HttpResponsePermanentRedirect(newurl)
else:
return response
@contextlib.contextmanager
def safe_query_string(request):
"""
Turn the QUERY_STRING into a unicode- and ascii-safe string.
We need unicode so it can be combined with a reversed URL, but it has to be
ascii to go in a Location header. iri_to_uri seems like a good compromise.
"""
qs = request.META['QUERY_STRING']
try:
request.META['QUERY_STRING'] = iri_to_uri(qs)
yield
finally:
request.META['QUERY_STRING'] = qs
class CommonMiddleware(common.CommonMiddleware):
def process_request(self, request):
with safe_query_string(request):
return super(CommonMiddleware, self).process_request(request)
class ReadOnlyMiddleware(object):
def process_request(self, request):
if request.method == 'POST':
return jingo.render(request, 'amo/read-only.html', status=503)
def process_exception(self, request, exception):
if isinstance(exception, mysql.OperationalError):
return jingo.render(request, 'amo/read-only.html', status=503)
pjax_log = commonware.log.getLogger('z.timer')
class LazyPjaxMiddleware(object):
def process_request(self, request):
# This activates JS in templates:
request.ALLOWS_PJAX = True
def process_response(self, request, response):
if (request.META.get('HTTP_X_PJAX') and
response.status_code == 200 and
'html' in response.get('content-type', '').lower()):
# TODO(Kumar) cache this.
with statsd.timer('pjax.parse'):
tree = lxml.html.document_fromstring(response.content)
# HTML is encoded as ascii with entity refs for non-ascii.
html = []
found_pjax = False
for elem in tree.cssselect('title,%s'
% settings.PJAX_SELECTOR):
if elem.tag == 'title':
# Inject a <title> for jquery-pjax
html.append(lxml.html.tostring(elem, encoding=None))
else:
found_pjax = True
if elem.text:
html.append(elem.text.encode('ascii',
'xmlcharrefreplace'))
for ch in elem.iterchildren():
html.append(lxml.html.tostring(ch, encoding=None))
if not found_pjax:
msg = ('pjax response for %s does not contain selector %r'
% (request.path, settings.PJAX_SELECTOR))
if settings.DEBUG:
# Tell the developer the template is bad.
raise ValueError(msg)
else:
pjax_log.error(msg)
return response
response.content = ''.join(html)
return response
class ViewMiddleware(object):
def get_name(self, view_func):
# Find a function name or used the class based view class name.
if not hasattr(view_func, '__name__'):
name = view_func.__class__.__name__
else:
name = view_func.__name__
return '%s.%s' % (view_func.__module__, name)
class LoginRequiredMiddleware(ViewMiddleware):
"""
If enabled, will force a login on all requests. Unless the view
is decorated with the no_login_required decorator, or placed
in the NO_LOGIN_REQUIRED_MODULES tuple.
"""
def process_view(self, request, view_func, view_args, view_kwargs):
name = self.get_name(view_func)
if (request.user.is_authenticated() or
getattr(view_func, '_no_login_required', False) or
name.startswith(settings.NO_LOGIN_REQUIRED_MODULES)):
return
return redirect('/%s/%s%s' % (request.LANG, request.APP.short,
settings.LOGIN_URL))
class NoAddonsMiddleware(ViewMiddleware):
"""
If enabled will try and stop any requests to addons by 404'ing them.
Here there be dragons. Fortunately this is temporary right?
"""
def process_view(self, request, view_func, view_args, view_kwargs):
name = self.get_name(view_func)
if name.startswith(settings.NO_ADDONS_MODULES):
raise Http404