2010-10-11 22:24:41 +04:00
|
|
|
import contextlib
|
|
|
|
import socket
|
|
|
|
import urllib
|
2011-01-26 20:06:45 +03:00
|
|
|
import urllib2
|
2010-10-11 22:24:41 +04:00
|
|
|
import urlparse
|
2011-09-29 06:17:24 +04:00
|
|
|
import re
|
2010-10-11 22:24:41 +04:00
|
|
|
|
|
|
|
from django.conf import settings
|
2010-10-22 21:55:29 +04:00
|
|
|
from django.utils.http import urlencode
|
2010-10-11 22:24:41 +04:00
|
|
|
|
2011-01-26 20:06:45 +03:00
|
|
|
import commonware.log
|
2011-06-21 00:21:28 +04:00
|
|
|
from statsd import statsd
|
2011-01-26 20:06:45 +03:00
|
|
|
|
|
|
|
from amo.helpers import absolutify
|
|
|
|
from amo.urlresolvers import reverse
|
|
|
|
|
|
|
|
|
|
|
|
class PaypalError(Exception):
|
|
|
|
id = None
|
|
|
|
|
|
|
|
|
2011-09-10 03:49:06 +04:00
|
|
|
class AuthError(PaypalError):
|
2011-01-26 20:06:45 +03:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
errors = {'520003': AuthError}
|
|
|
|
paypal_log = commonware.log.getLogger('z.paypal')
|
|
|
|
|
2011-09-29 03:49:05 +04:00
|
|
|
|
2011-09-10 03:49:06 +04:00
|
|
|
def should_ignore_paypal():
|
|
|
|
"""
|
|
|
|
Returns whether to skip PayPal communications for development
|
|
|
|
purposes or not.
|
|
|
|
"""
|
|
|
|
return settings.DEBUG and 'sandbox' not in settings.PAYPAL_PERMISSIONS_URL
|
|
|
|
|
2011-09-07 02:22:24 +04:00
|
|
|
|
2011-01-26 20:06:45 +03:00
|
|
|
def get_paykey(data):
|
|
|
|
"""
|
|
|
|
Gets a paykey from Paypal. Need to pass in the following in data:
|
2011-09-10 01:28:05 +04:00
|
|
|
pattern: the reverse pattern to resolve
|
2011-01-26 20:06:45 +03:00
|
|
|
email: who the money is going to (required)
|
|
|
|
amount: the amount of money (required)
|
|
|
|
ip: ip address of end user (required)
|
|
|
|
uuid: contribution_uuid (required)
|
|
|
|
memo: any nice message
|
2011-09-30 04:55:50 +04:00
|
|
|
qs: anything you want to append to the complete or cancel(optional)
|
2011-01-26 20:06:45 +03:00
|
|
|
"""
|
2011-09-10 01:28:05 +04:00
|
|
|
complete = reverse(data['pattern'], args=[data['slug'], 'complete'])
|
|
|
|
cancel = reverse(data['pattern'], args=[data['slug'], 'cancel'])
|
2011-09-30 04:55:50 +04:00
|
|
|
|
|
|
|
qs = {'uuid': data['uuid']}
|
|
|
|
if 'qs' in data:
|
|
|
|
qs.update(data['qs'])
|
|
|
|
uuid_qs = urllib.urlencode(qs)
|
2011-01-26 20:06:45 +03:00
|
|
|
|
|
|
|
paypal_data = {
|
|
|
|
'actionType': 'PAY',
|
|
|
|
'requestEnvelope.errorLanguage': 'US',
|
|
|
|
'currencyCode': 'USD',
|
2011-09-09 21:21:30 +04:00
|
|
|
'cancelUrl': absolutify('%s?%s' % (cancel, uuid_qs)),
|
|
|
|
'returnUrl': absolutify('%s?%s' % (complete, uuid_qs)),
|
2011-01-26 20:06:45 +03:00
|
|
|
'receiverList.receiver(0).email': data['email'],
|
|
|
|
'receiverList.receiver(0).amount': data['amount'],
|
|
|
|
'receiverList.receiver(0).invoiceID': 'mozilla-%s' % data['uuid'],
|
|
|
|
'receiverList.receiver(0).primary': 'TRUE',
|
|
|
|
'receiverList.receiver(0).paymentType': 'DIGITALGOODS',
|
2011-10-04 00:07:26 +04:00
|
|
|
'trackingId': data['uuid']}
|
|
|
|
|
|
|
|
if data.get('ipn', True):
|
|
|
|
paypal_data['ipnNotificationUrl'] = absolutify(reverse('amo.paypal'))
|
2011-01-26 20:06:45 +03:00
|
|
|
|
|
|
|
if data.get('memo'):
|
|
|
|
paypal_data['memo'] = data['memo']
|
|
|
|
|
2011-09-07 02:22:24 +04:00
|
|
|
with statsd.timer('paypal.paykey.retrieval'):
|
2011-09-09 21:21:30 +04:00
|
|
|
try:
|
2011-09-10 01:28:05 +04:00
|
|
|
response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
|
2011-09-09 21:21:30 +04:00
|
|
|
ip=data['ip'])
|
|
|
|
except AuthError, error:
|
2011-09-10 01:28:05 +04:00
|
|
|
paypal_log.error('Authentication error: %s' % error)
|
|
|
|
raise
|
2011-09-07 02:22:24 +04:00
|
|
|
return response['payKey']
|
|
|
|
|
|
|
|
|
2011-09-10 01:28:05 +04:00
|
|
|
def check_purchase(paykey):
|
|
|
|
"""
|
|
|
|
When a purchase is complete checks paypal that the purchase has gone
|
|
|
|
through.
|
|
|
|
"""
|
|
|
|
with statsd.timer('paypal.payment.details'):
|
|
|
|
try:
|
|
|
|
response = _call(settings.PAYPAL_PAY_URL + 'PaymentDetails',
|
|
|
|
{'payKey': paykey})
|
|
|
|
except PaypalError:
|
|
|
|
paypal_log.error('Payment details error', exc_info=True)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return response['status']
|
|
|
|
|
2011-10-04 00:07:26 +04:00
|
|
|
|
2011-09-29 06:17:24 +04:00
|
|
|
def refund(txnid):
|
|
|
|
"""
|
|
|
|
Refund a payment.
|
|
|
|
|
|
|
|
Arguments: transaction id of payment to refund
|
|
|
|
|
|
|
|
Returns: A list of dicts containing the refund info for each
|
|
|
|
receiver of the original payment.
|
|
|
|
"""
|
|
|
|
OK_STATUSES = ['REFUNDED', 'REFUNDED_PENDING']
|
|
|
|
with statsd.timer('paypal.payment.refund'):
|
|
|
|
try:
|
|
|
|
response = _call(settings.PAYPAL_PAY_URL + 'Refund',
|
|
|
|
{'transactionID': txnid})
|
|
|
|
except PaypalError:
|
|
|
|
paypal_log.error('Refund error', exc_info=True)
|
|
|
|
raise
|
|
|
|
responses = []
|
|
|
|
for k in response:
|
|
|
|
g = re.match('refundInfoList.refundInfo\((\d+)\).(.*)', k)
|
|
|
|
if g:
|
|
|
|
i = int(g.group(1))
|
|
|
|
subkey = g.group(2)
|
|
|
|
while i >= len(responses):
|
|
|
|
responses.append({})
|
|
|
|
responses[i][subkey] = response[k]
|
|
|
|
for d in responses:
|
2011-10-04 00:07:26 +04:00
|
|
|
status = '%s: %s' % (d['receiver.email'], d['refundStatus'])
|
2011-09-29 06:17:24 +04:00
|
|
|
if d['refundStatus'] not in OK_STATUSES:
|
2011-10-04 00:07:26 +04:00
|
|
|
raise PaypalError('Bad refund status for %s' % status)
|
|
|
|
paypal_log.debug('Refund done for transaction %s, status: %s'
|
|
|
|
% (txnid, status))
|
2011-09-29 06:17:24 +04:00
|
|
|
|
|
|
|
return responses
|
|
|
|
|
2011-09-10 01:28:05 +04:00
|
|
|
|
2011-09-07 02:22:24 +04:00
|
|
|
def check_refund_permission(token):
|
|
|
|
"""
|
|
|
|
Asks PayPal whether the PayPal ID for this account has granted
|
|
|
|
refund permission to us.
|
|
|
|
"""
|
|
|
|
# This is set in settings_test so we don't start calling PayPal
|
|
|
|
# by accident. Explicitly set this in your tests.
|
|
|
|
if not settings.PAYPAL_PERMISSIONS_URL:
|
|
|
|
return False
|
2011-09-29 03:49:05 +04:00
|
|
|
paypal_log.debug('Checking refund permission for token: %s..'
|
|
|
|
% token[:10])
|
2011-09-07 02:22:24 +04:00
|
|
|
try:
|
|
|
|
with statsd.timer('paypal.permissions.refund'):
|
|
|
|
r = _call(settings.PAYPAL_PERMISSIONS_URL + 'GetPermissions',
|
|
|
|
{'token': token})
|
2011-09-29 03:49:05 +04:00
|
|
|
except PaypalError, error:
|
|
|
|
paypal_log.debug('Paypal returned error for token: %s.. error: %s'
|
|
|
|
% (token[:10], error))
|
2011-09-07 02:22:24 +04:00
|
|
|
return False
|
|
|
|
# in the future we may ask for other permissions so let's just
|
|
|
|
# make sure REFUND is one of them.
|
2011-09-29 03:49:05 +04:00
|
|
|
paypal_log.debug('Paypal returned permissions for token: %s.. perms: %s'
|
|
|
|
% (token[:10], r))
|
2011-09-07 02:22:24 +04:00
|
|
|
return 'REFUND' in [v for (k, v) in r.iteritems()
|
|
|
|
if k.startswith('scope')]
|
|
|
|
|
|
|
|
|
|
|
|
def refund_permission_url(addon):
|
|
|
|
"""
|
|
|
|
Send permissions request to PayPal for refund privileges on
|
|
|
|
this addon's paypal account. Returns URL on PayPal site to visit.
|
|
|
|
"""
|
|
|
|
# This is set in settings_test so we don't start calling PayPal
|
|
|
|
# by accident. Explicitly set this in your tests.
|
|
|
|
if not settings.PAYPAL_PERMISSIONS_URL:
|
|
|
|
return ''
|
2011-09-29 21:03:33 +04:00
|
|
|
paypal_log.debug('Getting refund permission URL for addon: %s' % addon.pk)
|
|
|
|
|
2011-09-07 02:22:24 +04:00
|
|
|
with statsd.timer('paypal.permissions.url'):
|
|
|
|
url = reverse('devhub.addons.acquire_refund_permission',
|
|
|
|
args=[addon.slug])
|
2011-09-29 21:03:33 +04:00
|
|
|
try:
|
|
|
|
r = _call(settings.PAYPAL_PERMISSIONS_URL + 'RequestPermissions',
|
|
|
|
{'scope': 'REFUND', 'callback': absolutify(url)})
|
|
|
|
except PaypalError, e:
|
|
|
|
paypal_log.debug('Error on refund permission URL addon: %s, %s' %
|
|
|
|
(addon.pk, e))
|
|
|
|
if 'malformed' in str(e):
|
|
|
|
# PayPal is very picky about where they redirect users to.
|
|
|
|
# If you try and create a PayPal permissions URL on a
|
|
|
|
# zamboni that has a non-standard port number or a
|
|
|
|
# non-standard TLD, it will blow up with an error. We need
|
|
|
|
# to be able to at least visit these pages and alter them
|
|
|
|
# in dev, so this will give you a broken token that doesn't
|
|
|
|
# work, but at least the page will function.
|
|
|
|
r = {'token': 'wont-work-paypal-doesnt-like-your-domain'}
|
|
|
|
else:
|
|
|
|
raise
|
2011-09-07 02:22:24 +04:00
|
|
|
return (settings.PAYPAL_CGI_URL +
|
|
|
|
'?cmd=_grant-permission&request_token=%s' % r['token'])
|
|
|
|
|
|
|
|
|
|
|
|
def get_permissions_token(request_token, verification_code):
|
|
|
|
"""
|
|
|
|
Send request for permissions token, after user has granted the
|
|
|
|
requested permissions via the PayPal page we redirected them to.
|
|
|
|
"""
|
|
|
|
with statsd.timer('paypal.permissions.token'):
|
|
|
|
r = _call(settings.PAYPAL_PERMISSIONS_URL + 'GetAccessToken',
|
|
|
|
{'token': request_token, 'verifier': verification_code})
|
|
|
|
return r['token']
|
|
|
|
|
|
|
|
|
|
|
|
def _call(url, paypal_data, ip=None):
|
|
|
|
request = urllib2.Request(url)
|
|
|
|
|
|
|
|
if 'requestEnvelope.errorLanguage' not in paypal_data:
|
|
|
|
paypal_data['requestEnvelope.errorLanguage'] = 'en_US'
|
|
|
|
|
|
|
|
for key, value in [
|
|
|
|
('security-userid', settings.PAYPAL_EMBEDDED_AUTH['USER']),
|
|
|
|
('security-password', settings.PAYPAL_EMBEDDED_AUTH['PASSWORD']),
|
|
|
|
('security-signature', settings.PAYPAL_EMBEDDED_AUTH['SIGNATURE']),
|
|
|
|
('application-id', settings.PAYPAL_APP_ID),
|
|
|
|
('request-data-format', 'NV'),
|
|
|
|
('response-data-format', 'NV')]:
|
|
|
|
request.add_header('X-PAYPAL-%s' % key.upper(), value)
|
|
|
|
|
|
|
|
if ip:
|
|
|
|
request.add_header('X-PAYPAL-DEVICE-IPADDRESS', ip)
|
|
|
|
|
2011-01-26 20:06:45 +03:00
|
|
|
opener = urllib2.build_opener()
|
|
|
|
try:
|
|
|
|
with socket_timeout(10):
|
|
|
|
feeddata = opener.open(request, urlencode(paypal_data)).read()
|
|
|
|
except Exception, error:
|
|
|
|
paypal_log.error('HTTP Error: %s' % error)
|
|
|
|
raise
|
|
|
|
|
|
|
|
response = dict(urlparse.parse_qsl(feeddata))
|
|
|
|
|
|
|
|
if 'error(0).errorId' in response:
|
|
|
|
error = errors.get(response['error(0).errorId'], PaypalError)
|
|
|
|
paypal_log.error('Paypal Error: %s' % response['error(0).message'])
|
|
|
|
raise error(response['error(0).message'])
|
|
|
|
|
2011-09-07 02:22:24 +04:00
|
|
|
return response
|
2011-01-26 20:06:45 +03:00
|
|
|
|
2010-10-11 22:24:41 +04:00
|
|
|
|
|
|
|
def check_paypal_id(name):
|
|
|
|
"""
|
|
|
|
Use the button API to check if name is a valid Paypal id.
|
|
|
|
|
|
|
|
Returns bool(valid), str(msg). msg will be None if there wasn't an error.
|
|
|
|
"""
|
2011-03-21 22:58:16 +03:00
|
|
|
d = dict(version=settings.PAYPAL_API_VERSION,
|
2010-10-11 22:24:41 +04:00
|
|
|
buttoncode='cleartext',
|
|
|
|
buttontype='donate',
|
|
|
|
method='BMCreateButton',
|
|
|
|
l_buttonvar0='business=%s' % name)
|
2011-03-21 22:58:16 +03:00
|
|
|
# TODO(andym): remove this once this is all live and settled down.
|
|
|
|
if hasattr(settings, 'PAYPAL_CGI_AUTH'):
|
|
|
|
d['user'] = settings.PAYPAL_CGI_AUTH['USER']
|
|
|
|
d['pwd'] = settings.PAYPAL_CGI_AUTH['PASSWORD']
|
|
|
|
d['signature'] = settings.PAYPAL_CGI_AUTH['SIGNATURE']
|
|
|
|
else:
|
|
|
|
# In production if PAYPAL_CGI_AUTH doesn't get defined yet,
|
|
|
|
# fall back to the existing values.
|
|
|
|
d.update(dict(user=settings.PAYPAL_USER,
|
|
|
|
pwd=settings.PAYPAL_PASSWORD,
|
|
|
|
signature=settings.PAYPAL_SIGNATURE))
|
2010-10-11 22:24:41 +04:00
|
|
|
with socket_timeout(10):
|
2010-10-22 21:55:29 +04:00
|
|
|
r = urllib.urlopen(settings.PAYPAL_API_URL, urlencode(d))
|
2010-10-11 22:24:41 +04:00
|
|
|
response = dict(urlparse.parse_qsl(r.read()))
|
|
|
|
valid = response['ACK'] == 'Success'
|
|
|
|
msg = None if valid else response['L_LONGMESSAGE0']
|
|
|
|
return valid, msg
|
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def socket_timeout(timeout):
|
|
|
|
"""Context manager to temporarily set the default socket timeout."""
|
|
|
|
old = socket.getdefaulttimeout()
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
socket.setdefaulttimeout(old)
|