<commit_before># -*- coding: utf-8 -*-
import contextlib
from decimal import Decimal, InvalidOperation
import socket
import urllib
import urllib2
import urlparse
import re

from django.conf import settings
from django.utils.http import urlencode, urlquote

import commonware.log
from django_statsd.clients import statsd

from amo.helpers import absolutify, loc, urlparams
from amo.urlresolvers import reverse
from amo.utils import log_cef
from tower import ugettext as _


class PaypalError(Exception):
    # The generic Paypal error and message.
    def __init__(self, id=None):
        super(PaypalError, self).__init__('')
        self.id = id

    def __str__(self):
        default = _('There was an error communicating with PayPal. '
                    'Please try again later.')
        msg = getattr(self, 'msg')
        if msg:
            return msg
        return messages.get(self.id, default)


class PaypalDataError(PaypalError):
    # Some of the data passed to Paypal was incorrect. We'll catch them and
    # re-raise as a PaypalError so they can be easily caught.

    def __init__(self, msg):
        self.msg = msg

class AuthError(PaypalError):
    # We've got the settings wrong on our end.
    pass


class PreApprovalError(PaypalError):
    # Something went wrong in pre approval, there's usually not much
    # we can do about this.
    pass


errors = {'520003': AuthError}
# See http://bit.ly/vWV525 for information on these values.
# Note that if you have and invalid preapproval key you get 580022, but this
# also occurs in other cases so don't assume its preapproval only.
for number in ['579024', '579025', '579026', '579027', '579028',
               '579030', '579031']:
    errors[number] = PreApprovalError

# Here you can map PayPal error messages into hopefully more useful
# error messages.
messages = {'589023': loc('The amount is too small for conversion '
                          'into the receviers currency.')}


paypal_log = commonware.log.getLogger('z.paypal')


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


def add_receivers(chains, email, amount, uuid, preapproval=False):
    """
    Split a payment down into multiple receivers using the chains passed in.
    """
    try:
        remainder = Decimal(str(amount))
    except InvalidOperation, msg:
        raise PaypalDataError(msg)

    result = {}
    for number, chain in enumerate(chains, 1):
        percent, destination = chain
        this = (Decimal(str(float(amount) * (percent / 100.0)))
                .quantize(Decimal('.01')))
        remainder = remainder - this
        key = 'receiverList.receiver(%s)' % number
        result.update({
            '%s.email' % key: destination,
            '%s.amount' % key: str(this),
            '%s.primary' % key: 'false',
            # This is only done if there is a chained payment. Otherwise
            # it does not need to be set.
            'receiverList.receiver(0).primary': 'true',
            # Mozilla pays the fees, because we've got a special rate.
            'feesPayer': 'SECONDARYONLY'
        })
        if not preapproval:
            result['%s.paymentType' % key] = 'DIGITALGOODS'

    result.update({
        'receiverList.receiver(0).email': email,
        'receiverList.receiver(0).amount': str(amount),
        'receiverList.receiver(0).invoiceID': 'mozilla-%s' % uuid
    })

    # Adding DIGITALGOODS to a pre-approval triggers an error in PayPal.
    if not preapproval:
        result['receiverList.receiver(0).paymentType'] = 'DIGITALGOODS'

    return result


def get_paykey(data):
    """
    Gets a paykey from Paypal. Need to pass in the following in data:
    pattern: the reverse pattern to resolve
    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
    qs: anything you want to append to the complete or cancel(optional)
    """
    complete = reverse(data['pattern'], args=[data['slug'], 'complete'])
    cancel = reverse(data['pattern'], args=[data['slug'], 'cancel'])

    qs = {'uuid': data['uuid']}
    if 'qs' in data:
        qs.update(data['qs'])
    uuid_qs = urllib.urlencode(qs)

    paypal_data = {
        'actionType': 'PAY',
        'currencyCode': 'USD',
        'cancelUrl': absolutify('%s?%s' % (cancel, uuid_qs)),
        'returnUrl': absolutify('%s?%s' % (complete, uuid_qs)),
        'trackingId': data['uuid'],
        'ipnNotificationUrl': absolutify(reverse('amo.paypal'))}

    receivers = (data.get('chains', ()), data['email'], data['amount'],
                 data['uuid'])

    if data.get('preapproval'):
        # The paypal_key might be empty if they have removed it.
        key = data['preapproval'].paypal_key
        if key:
            paypal_log.info('Using preapproval: %s' % data['preapproval'].pk)
            paypal_data['preapprovalKey'] = key

    paypal_data.update(add_receivers(*receivers,
                                preapproval='preapprovalKey' in paypal_data))

    if data.get('memo'):
        paypal_data['memo'] = data['memo']

    try:
        with statsd.timer('paypal.paykey.retrieval'):
            response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
                             ip=data['ip'])
    except PreApprovalError, e:
        # Let's retry just once without preapproval.
        paypal_log.error('Failed using preapproval, reason: %s' % e)
        # Now it's not a pre-approval, make sure we get the
        # DIGITALGOODS setting back in there.
        del paypal_data['preapprovalKey']
        paypal_data.update(add_receivers(*receivers))
        # If this fails, we won't try again, just fail.
        with statsd.timer('paypal.paykey.retrieval'):
            response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
                             ip=data['ip'])

    return response['payKey'], response['paymentExecStatus']


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']


def refund(paykey):
    """
    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',
                             {'payKey': paykey})
        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:
            if d['refundStatus'] not in OK_STATUSES:
                raise PaypalError('Bad refund status for %s: %s'
                                  % (d['receiver.email'],
                                     d['refundStatus']))
            paypal_log.debug('Refund successful for: %s, %s, %s' %
                             (paykey, d['receiver.email'], d['refundStatus']))

        return responses


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
    paypal_log.debug('Checking refund permission for token: %s..'
                     % token[:10])
    try:
        with statsd.timer('paypal.permissions.refund'):
            r = _call(settings.PAYPAL_PERMISSIONS_URL + 'GetPermissions',
                      {'token': token})
    except PaypalError, error:
        paypal_log.debug('Paypal returned error for token: %s.. error: %s'
                         % (token[:10], error))
        return False
    # in the future we may ask for other permissions so let's just
    # make sure REFUND is one of them.
    paypal_log.debug('Paypal returned permissions for token: %s.. perms: %s'
                     % (token[:10], r))
    return 'REFUND' in [v for (k, v) in r.iteritems()
                        if k.startswith('scope')]


def refund_permission_url(addon, dest='payments'):
    """
    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 ''
    paypal_log.debug('Getting refund permission URL for addon: %s' % addon.pk)

    with statsd.timer('paypal.permissions.url'):
        url = urlparams(reverse('devhub.addons.acquire_refund_permission',
                                args=[addon.slug]),
                        dest=dest)
        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 e.id == '580028':
                # 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': '<PASSWORD>'}
            else:
                raise
    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 get_preapproval_key(data):
    """
    Get a preapproval key from PayPal. If this passes, you get a key that
    you can use in a redirect to PayPal.
    """
    paypal_data = {
        'currencyCode': 'USD',
        'startingDate': data['startDate'].strftime('%Y-%m-%d'),
        'endingDate': data['endDate'].strftime('%Y-%m-%d'),
        'maxTotalAmountOfAllPayments': str(data.get('maxAmount', '2000')),
        'returnUrl': absolutify(reverse(data['pattern'], args=['complete'])),
        'cancelUrl': absolutify(reverse(data['pattern'], args=['cancel'])),
    }
    with statsd.timer('paypal.preapproval.token'):
        response = _call(settings.PAYPAL_PAY_URL + 'Preapproval', paypal_data,
                         ip=data.get('ip'))

    return response


def get_preapproval_url(key):
    """
    Returns the URL that you need to bounce user to in order to set up
    pre-approval.
    """
    return urlparams(settings.PAYPAL_CGI_URL, cmd='_ap-preapproval',
                     preapprovalkey=key)


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)

    # Warning, a urlencode will not work with chained payments, it must
    # be sorted and the key should not be escaped.
    data = '&'.join(['%s=%s' % (k, urlquote(v))
                     for k, v in sorted(paypal_data.items())])
    opener = urllib2.build_opener()
    try:
        with socket_timeout(10):
            feeddata = opener.open(request, data).read()
    except AuthError, error:
        paypal_log.error('Authentication error: %s' % error)
        raise
    except Exception, error:
        paypal_log.error('HTTP Error: %s' % error)
        # We'll log the actual error and then raise a Paypal error.
        # That way all the calling methods only have catch a Paypal error,
        # the fact that there may be say, a http error, is internal to this
        # method.
        raise PaypalError

    response = dict(urlparse.parse_qsl(feeddata))

    if 'error(0).errorId' in response:
        id_, msg = response['error(0).errorId'], response['error(0).message']
        paypal_log.error('Paypal Error (%s): %s' % (id_, msg))
        raise errors.get(id_, PaypalError)(id_)

    return response


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.
    """
    d = dict(version=settings.PAYPAL_API_VERSION,
             buttoncode='cleartext',
             buttontype='donate',
             method='BMCreateButton',
             l_buttonvar0='business=%s' % name)
    d['user'] = settings.PAYPAL_EMBEDDED_AUTH['USER']
    d['pwd'] = settings.PAYPAL_EMBEDDED_AUTH['PASSWORD']
    d['signature'] = settings.PAYPAL_EMBEDDED_AUTH['SIGNATURE']
    with socket_timeout(10):
        r = urllib.urlopen(settings.PAYPAL_API_URL, urlencode(d))
    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)


def paypal_log_cef(request, addon, uuid, msg, caps, longer):
    log_cef('Paypal %s' % msg, 5, request,
            username=request.amo_user,
            signature='PAYPAL%s' % caps,
            msg=longer, cs2=addon.name, cs2Label='PaypalTransaction',
            cs4=uuid, cs4Label='TxID')
<commit_msg>provide a default<commit_after># -*- coding: utf-8 -*-
import contextlib
from decimal import Decimal, InvalidOperation
import socket
import urllib
import urllib2
import urlparse
import re

from django.conf import settings
from django.utils.http import urlencode, urlquote

import commonware.log
from django_statsd.clients import statsd

from amo.helpers import absolutify, loc, urlparams
from amo.urlresolvers import reverse
from amo.utils import log_cef
from tower import ugettext as _


class PaypalError(Exception):
    # The generic Paypal error and message.
    def __init__(self, id=None):
        super(PaypalError, self).__init__('')
        self.id = id

    def __str__(self):
        default = _('There was an error communicating with PayPal. '
                    'Please try again later.')
        msg = getattr(self, 'msg', None)
        if msg:
            return msg
        return messages.get(self.id, default)


class PaypalDataError(PaypalError):
    # Some of the data passed to Paypal was incorrect. We'll catch them and
    # re-raise as a PaypalError so they can be easily caught.

    def __init__(self, msg):
        self.msg = msg

class AuthError(PaypalError):
    # We've got the settings wrong on our end.
    pass


class PreApprovalError(PaypalError):
    # Something went wrong in pre approval, there's usually not much
    # we can do about this.
    pass


errors = {'520003': AuthError}
# See http://bit.ly/vWV525 for information on these values.
# Note that if you have and invalid preapproval key you get 580022, but this
# also occurs in other cases so don't assume its preapproval only.
for number in ['579024', '579025', '579026', '579027', '579028',
               '579030', '579031']:
    errors[number] = PreApprovalError

# Here you can map PayPal error messages into hopefully more useful
# error messages.
messages = {'589023': loc('The amount is too small for conversion '
                          'into the receviers currency.')}


paypal_log = commonware.log.getLogger('z.paypal')


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


def add_receivers(chains, email, amount, uuid, preapproval=False):
    """
    Split a payment down into multiple receivers using the chains passed in.
    """
    try:
        remainder = Decimal(str(amount))
    except InvalidOperation, msg:
        raise PaypalDataError(msg)

    result = {}
    for number, chain in enumerate(chains, 1):
        percent, destination = chain
        this = (Decimal(str(float(amount) * (percent / 100.0)))
                .quantize(Decimal('.01')))
        remainder = remainder - this
        key = 'receiverList.receiver(%s)' % number
        result.update({
            '%s.email' % key: destination,
            '%s.amount' % key: str(this),
            '%s.primary' % key: 'false',
            # This is only done if there is a chained payment. Otherwise
            # it does not need to be set.
            'receiverList.receiver(0).primary': 'true',
            # Mozilla pays the fees, because we've got a special rate.
            'feesPayer': 'SECONDARYONLY'
        })
        if not preapproval:
            result['%s.paymentType' % key] = 'DIGITALGOODS'

    result.update({
        'receiverList.receiver(0).email': email,
        'receiverList.receiver(0).amount': str(amount),
        'receiverList.receiver(0).invoiceID': 'mozilla-%s' % uuid
    })

    # Adding DIGITALGOODS to a pre-approval triggers an error in PayPal.
    if not preapproval:
        result['receiverList.receiver(0).paymentType'] = 'DIGITALGOODS'

    return result


def get_paykey(data):
    """
    Gets a paykey from Paypal. Need to pass in the following in data:
    pattern: the reverse pattern to resolve
    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
    qs: anything you want to append to the complete or cancel(optional)
    """
    complete = reverse(data['pattern'], args=[data['slug'], 'complete'])
    cancel = reverse(data['pattern'], args=[data['slug'], 'cancel'])

    qs = {'uuid': data['uuid']}
    if 'qs' in data:
        qs.update(data['qs'])
    uuid_qs = urllib.urlencode(qs)

    paypal_data = {
        'actionType': 'PAY',
        'currencyCode': 'USD',
        'cancelUrl': absolutify('%s?%s' % (cancel, uuid_qs)),
        'returnUrl': absolutify('%s?%s' % (complete, uuid_qs)),
        'trackingId': data['uuid'],
        'ipnNotificationUrl': absolutify(reverse('amo.paypal'))}

    receivers = (data.get('chains', ()), data['email'], data['amount'],
                 data['uuid'])

    if data.get('preapproval'):
        # The paypal_key might be empty if they have removed it.
        key = data['preapproval'].paypal_key
        if key:
            paypal_log.info('Using preapproval: %s' % data['preapproval'].pk)
            paypal_data['preapprovalKey'] = key

    paypal_data.update(add_receivers(*receivers,
                                preapproval='preapprovalKey' in paypal_data))

    if data.get('memo'):
        paypal_data['memo'] = data['memo']

    try:
        with statsd.timer('paypal.paykey.retrieval'):
            response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
                             ip=data['ip'])
    except PreApprovalError, e:
        # Let's retry just once without preapproval.
        paypal_log.error('Failed using preapproval, reason: %s' % e)
        # Now it's not a pre-approval, make sure we get the
        # DIGITALGOODS setting back in there.
        del paypal_data['preapprovalKey']
        paypal_data.update(add_receivers(*receivers))
        # If this fails, we won't try again, just fail.
        with statsd.timer('paypal.paykey.retrieval'):
            response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
                             ip=data['ip'])

    return response['payKey'], response['paymentExecStatus']


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']


def refund(paykey):
    """
    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',
                             {'payKey': paykey})
        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:
            if d['refundStatus'] not in OK_STATUSES:
                raise PaypalError('Bad refund status for %s: %s'
                                  % (d['receiver.email'],
                                     d['refundStatus']))
            paypal_log.debug('Refund successful for: %s, %s, %s' %
                             (paykey, d['receiver.email'], d['refundStatus']))

        return responses


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
    paypal_log.debug('Checking refund permission for token: %s..'
                     % token[:10])
    try:
        with statsd.timer('paypal.permissions.refund'):
            r = _call(settings.PAYPAL_PERMISSIONS_URL + 'GetPermissions',
                      {'token': token})
    except PaypalError, error:
        paypal_log.debug('Paypal returned error for token: %s.. error: %s'
                         % (token[:10], error))
        return False
    # in the future we may ask for other permissions so let's just
    # make sure REFUND is one of them.
    paypal_log.debug('Paypal returned permissions for token: %s.. perms: %s'
                     % (token[:10], r))
    return 'REFUND' in [v for (k, v) in r.iteritems()
                        if k.startswith('scope')]


def refund_permission_url(addon, dest='payments'):
    """
    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 ''
    paypal_log.debug('Getting refund permission URL for addon: %s' % addon.pk)

    with statsd.timer('paypal.permissions.url'):
        url = urlparams(reverse('devhub.addons.acquire_refund_permission',
                                args=[addon.slug]),
                        dest=dest)
        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 e.id == '580028':
                # 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': '<PASSWORD>'}
            else:
                raise
    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 get_preapproval_key(data):
    """
    Get a preapproval key from PayPal. If this passes, you get a key that
    you can use in a redirect to PayPal.
    """
    paypal_data = {
        'currencyCode': 'USD',
        'startingDate': data['startDate'].strftime('%Y-%m-%d'),
        'endingDate': data['endDate'].strftime('%Y-%m-%d'),
        'maxTotalAmountOfAllPayments': str(data.get('maxAmount', '2000')),
        'returnUrl': absolutify(reverse(data['pattern'], args=['complete'])),
        'cancelUrl': absolutify(reverse(data['pattern'], args=['cancel'])),
    }
    with statsd.timer('paypal.preapproval.token'):
        response = _call(settings.PAYPAL_PAY_URL + 'Preapproval', paypal_data,
                         ip=data.get('ip'))

    return response


def get_preapproval_url(key):
    """
    Returns the URL that you need to bounce user to in order to set up
    pre-approval.
    """
    return urlparams(settings.PAYPAL_CGI_URL, cmd='_ap-preapproval',
                     preapprovalkey=key)


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)

    # Warning, a urlencode will not work with chained payments, it must
    # be sorted and the key should not be escaped.
    data = '&'.join(['%s=%s' % (k, urlquote(v))
                     for k, v in sorted(paypal_data.items())])
    opener = urllib2.build_opener()
    try:
        with socket_timeout(10):
            feeddata = opener.open(request, data).read()
    except AuthError, error:
        paypal_log.error('Authentication error: %s' % error)
        raise
    except Exception, error:
        paypal_log.error('HTTP Error: %s' % error)
        # We'll log the actual error and then raise a Paypal error.
        # That way all the calling methods only have catch a Paypal error,
        # the fact that there may be say, a http error, is internal to this
        # method.
        raise PaypalError

    response = dict(urlparse.parse_qsl(feeddata))

    if 'error(0).errorId' in response:
        id_, msg = response['error(0).errorId'], response['error(0).message']
        paypal_log.error('Paypal Error (%s): %s' % (id_, msg))
        raise errors.get(id_, PaypalError)(id_)

    return response


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.
    """
    d = dict(version=settings.PAYPAL_API_VERSION,
             buttoncode='cleartext',
             buttontype='donate',
             method='BMCreateButton',
             l_buttonvar0='business=%s' % name)
    d['user'] = settings.PAYPAL_EMBEDDED_AUTH['USER']
    d['pwd'] = settings.PAYPAL_EMBEDDED_AUTH['PASSWORD']
    d['signature'] = settings.PAYPAL_EMBEDDED_AUTH['SIGNATURE']
    with socket_timeout(10):
        r = urllib.urlopen(settings.PAYPAL_API_URL, urlencode(d))
    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)


def paypal_log_cef(request, addon, uuid, msg, caps, longer):
    log_cef('Paypal %s' % msg, 5, request,
            username=request.amo_user,
            signature='PAYPAL%s' % caps,
            msg=longer, cs2=addon.name, cs2Label='PaypalTransaction',
            cs4=uuid, cs4Label='TxID')

{'fraction_non_alphanumeric': 0.1124312075105212, 'fraction_numerical': 0.007186791842020071, 'mean_word_length': 2.463116591928251, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3478638', 'n_tokens_mistral': 9424, 'n_tokens_neox': 9056, 'n_words': 2627}