addons-server/apps/amo/views.py

265 строки
9.1 KiB
Python

import json
import random
import urllib2
from django import http
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import commonware.log
import jingo
import phpserialize as php
import waffle
from django_arecibo.tasks import post
from statsd import statsd
import amo
import files.tasks
from amo.decorators import post_required
from stats.models import Contribution, ContributionError, SubscriptionEvent
from . import monitors
monitor_log = commonware.log.getLogger('z.monitor')
paypal_log = commonware.log.getLogger('z.paypal')
csp_log = commonware.log.getLogger('z.csp')
jp_log = commonware.log.getLogger('z.jp.repack')
@never_cache
def monitor(request, format=None):
# For each check, a boolean pass/fail status to show in the template
status_summary = {}
results = {}
checks = ['memcache', 'libraries', 'elastic', 'path', 'redis', 'hera']
for check in checks:
with statsd.timer('monitor.%s' % check) as timer:
status, result = getattr(monitors, check)()
status_summary[check] = status
results['%s_results' % check] = result
results['%s_timer' % check] = timer.ms
# If anything broke, send HTTP 500.
status_code = 200 if all(status_summary.values()) else 500
if format == '.json':
return http.HttpResponse(json.dumps(status_summary),
status=status_code)
ctx = {}
ctx.update(results)
ctx['status_summary'] = status_summary
return jingo.render(request, 'services/monitor.html',
ctx, status=status_code)
def robots(request):
"""Generate a robots.txt"""
_service = (request.META['SERVER_NAME'] == settings.SERVICES_DOMAIN)
if _service or not settings.ENGAGE_ROBOTS:
template = "User-agent: *\nDisallow: /"
else:
template = jingo.render(request, 'amo/robots.html',
{'apps': amo.APP_USAGE})
return HttpResponse(template, mimetype="text/plain")
@csrf_exempt
def paypal(request):
"""
Handle PayPal IPN post-back for contribution transactions.
IPN will retry periodically until it gets success (status=200). Any
db errors or replication lag will result in an exception and http
status of 500, which is good so PayPal will try again later.
PayPal IPN variables available at:
https://cms.paypal.com/us/cgi-bin/?cmd=_render-content
&content_ID=developer/e_howto_html_IPNandPDTVariables
"""
try:
return _paypal(request)
except Exception, e:
paypal_log.error('%s\n%s' % (e, request))
return http.HttpResponseServerError('Unknown error.')
def _paypal(request):
def _log_error_with_data(msg, post):
"""Log a message along with some of the POST info from PayPal."""
id = random.randint(0, 99999999)
msg = "[%s] %s (dumping data)" % (id, msg)
paypal_log.error(msg)
logme = {'txn_id': post.get('txn_id'),
'txn_type': post.get('txn_type'),
'payer_email': post.get('payer_email'),
'receiver_email': post.get('receiver_email'),
'payment_status': post.get('payment_status'),
'payment_type': post.get('payment_type'),
'mc_gross': post.get('mc_gross'),
'item_number': post.get('item_number'),
}
paypal_log.error("[%s] PayPal Data: %s" % (id, logme))
if request.method != 'POST':
return http.HttpResponseNotAllowed(['POST'])
# raw_post_data has to be accessed before request.POST. wtf django?
raw, post = request.raw_post_data, request.POST.copy()
# Check that the request is valid and coming from PayPal.
# The order of the params has to match the original request.
data = u'cmd=_notify-validate&' + raw
paypal_response = urllib2.urlopen(settings.PAYPAL_CGI_URL,
data, 20).readline()
# List of (old, new) codes so we can transpose the data for
# embedded payments.
for old, new in [('payment_status', 'status'),
('item_number', 'tracking_id'),
('txn_id', 'tracking_id'),
('payer_email', 'sender_email')]:
if old not in post and new in post:
post[old] = post[new]
if paypal_response != 'VERIFIED':
msg = ("Expecting 'VERIFIED' from PayPal, got '%s'. "
"Failing." % paypal_response)
_log_error_with_data(msg, post)
return http.HttpResponseForbidden('Invalid confirmation')
if post.get('txn_type', '').startswith('subscr_'):
SubscriptionEvent.objects.create(post_data=php.serialize(post))
return http.HttpResponse('Success!')
# We only care about completed transactions.
if post.get('payment_status', '').lower() != 'completed':
return http.HttpResponse('Payment not completed')
# Make sure transaction has not yet been processed.
if (Contribution.objects
.filter(transaction_id=post['txn_id']).count()) > 0:
return http.HttpResponse('Transaction already processed')
# Fetch and update the contribution - item_number is the uuid we created.
try:
c = Contribution.objects.get(uuid=post['item_number'])
except Contribution.DoesNotExist:
key = "%s%s:%s" % (settings.CACHE_PREFIX, 'contrib',
post['item_number'])
count = cache.get(key, 0) + 1
paypal_log.warning('Contribution (uuid=%s) not found for IPN request '
'#%s.' % (post['item_number'], count))
if count > 10:
msg = ("PayPal sent a transaction that we don't know "
"about and we're giving up on it.")
_log_error_with_data(msg, post)
cache.delete(key)
return http.HttpResponse('Transaction not found; skipping.')
cache.set(key, count, 1209600) # This is 2 weeks.
return http.HttpResponseServerError('Contribution not found')
c.transaction_id = post['txn_id']
# Embedded payments does not send an mc_gross.
if 'mc_gross' in post:
c.amount = post['mc_gross']
c.uuid = None
c.post_data = php.serialize(post)
c.save()
# Send thankyou email.
try:
c.mail_thankyou(request)
except ContributionError as e:
# A failed thankyou email is not a show stopper, but is good to know.
paypal_log.error('Thankyou note email failed with error: %s' % e)
return http.HttpResponse('Success!')
def handler404(request):
return jingo.render(request, 'amo/404.html', status=404)
def handler500(request):
arecibo = getattr(settings, 'ARECIBO_SERVER_URL', '')
if arecibo:
post(request, 500)
return jingo.render(request, 'amo/500.html', status=500)
def csrf_failure(request, reason=''):
return jingo.render(request, 'amo/403.html',
{'csrf': 'CSRF' in reason}, status=403)
def loaded(request):
return http.HttpResponse('%s' % request.META['wsgi.loaded'],
content_type='text/plain')
@csrf_exempt
@require_POST
def cspreport(request):
"""Accept CSP reports and log them."""
report = ('blocked-uri', 'violated-directive', 'original-policy')
if not waffle.sample_is_active('csp-store-reports'):
return HttpResponse()
try:
v = json.loads(request.raw_post_data)['csp-report']
# CEF module wants a dictionary of environ, we want request
# to be the page with error on it, that's contained in the csp-report.
meta = request.META.copy()
method, url = v['request'].split(' ', 1)
meta.update({'REQUEST_METHOD': method, 'PATH_INFO': url})
v = [(k, v[k]) for k in report if k in v]
# This requires you to use the cef.formatter to get something nice out.
csp_log.warning('Violation', dict(environ=meta,
product='addons',
username=request.user,
data=v))
except Exception:
return HttpResponseBadRequest()
return HttpResponse()
@csrf_exempt
@post_required
def builder_pingback(request):
data = dict(request.POST.items())
jp_log.info('Pingback from builder: %r' % data)
try:
# We expect all these attributes to be available.
attrs = 'result msg location secret request'.split()
for attr in attrs:
assert attr in data, '%s not in %s' % (attr, data)
# Only AMO and the builder should know this secret.
assert data.get('secret') == settings.BUILDER_SECRET_KEY
except Exception:
jp_log.warning('Problem with builder pingback.', exc_info=True)
return http.HttpResponseBadRequest()
files.tasks.repackage_jetpack(data)
return http.HttpResponse()
def graphite(request, site):
ctx = {'width': 586, 'height': 308}
ctx.update(request.GET.items())
ctx['site'] = site
return jingo.render(request, 'services/graphite.html', ctx)