[FIX] payment_paypal: accept PDT requests on /dpn route

It seems Paypal does not always send the same responses on auto-return
even when PDT is off. Although not reproducible on a Paypal sandbox,
sometimes the system auto-return to /payment/paypal/dpn without any
meaningful POST data. This seems to only happen with new accounts
that use the 'Hermes' web application of Paypal.

The correct thing to do would be to add a new field on the paypal
payment provider for PDT token and make the PDT flow available
to users; but this is a stable branch and this fix is already
sufficiently delicate. This shall be done in master though.

From this revision on, users can then activate PDT on their paypal
account, set the PDT token as an ir.config_parameter value (WITH
GROUP RESTRICTION SET TO ADMIN/SETTINGS GROUP!!!) and the system
will process these requests correctly.
This commit is contained in:
Damien Bouvy 2016-10-28 13:21:04 +02:00
parent 8e8b7925d2
commit 205b5542ca
No known key found for this signature in database
GPG Key ID: 1D0AB759B4B928E3
1 changed files with 30 additions and 7 deletions

View File

@ -24,18 +24,33 @@ class PaypalController(http.Controller):
""" Extract the return URL from the data coming from paypal. """
return_url = post.pop('return_url', '')
if not return_url:
custom = json.loads(post.pop('custom', False) or '{}')
custom = json.loads(post.pop('custom', False) or post.pop('cm', False) or '{}')
return_url = custom.get('return_url', '/')
return return_url
def _parse_pdt_response(self, response):
""" Parse a text reponse for a PDT verification .
:param response str: text response, structured in the following way:
STATUS\nkey1=value1\nkey2=value2...\n
:rtype tuple(str, dict)
:return: tuple containing the STATUS str and the key/value pairs
parsed as a dict
"""
lines = filter(None, response.split('\n'))
status = lines.pop(0)
pdt_post = dict(line.split('=', 1) for line in lines)
return status, pdt_post
def paypal_validate_data(self, **post):
""" Paypal IPN: three steps validation to ensure data correctness
- step 1: return an empty HTTP 200 response -> will be done at the end
by returning ''
- step 2: POST the complete, unaltered message back to Paypal (preceded
by cmd=_notify-validate), with same encoding
- step 3: paypal send either VERIFIED or INVALID (single word)
by cmd=_notify-validate or _notify-synch for PDT), with same encoding
- step 3: paypal send either VERIFIED or INVALID (single word) for IPN
or SUCCESS or FAIL (+ data) for PDT
Once data is validated, process it. """
res = False
@ -47,18 +62,26 @@ class PaypalController(http.Controller):
tx_ids = request.registry['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
if tx_ids:
tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
pdt_request = bool(new_post.get('amt')) # check for spefific pdt param
if pdt_request:
# this means we are in PDT instead of DPN like before
# fetch the PDT token
new_post['at'] = request.registry['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'payment_paypal.pdt_token')
new_post['cmd'] = '_notify-synch' # command is different in PDT than IPN/DPN
paypal_urls = request.registry['payment.acquirer']._get_paypal_urls(cr, uid, tx and tx.acquirer_id and tx.acquirer_id.environment or 'prod', context=context)
validate_url = paypal_urls['paypal_form_url']
urequest = urllib2.Request(validate_url, werkzeug.url_encode(new_post))
uopen = urllib2.urlopen(urequest)
resp = uopen.read()
if resp == 'VERIFIED':
if pdt_request:
resp, post = self._parse_pdt_response(resp)
if resp == 'VERIFIED' or pdt_request and resp == 'SUCCESS':
_logger.info('Paypal: validated data')
res = request.registry['payment.transaction'].form_feedback(cr, SUPERUSER_ID, post, 'paypal', context=context)
elif resp == 'INVALID':
_logger.warning('Paypal: answered INVALID on data verification')
elif resp == 'INVALID' or pdt_request and resp == 'FAIL':
_logger.warning('Paypal: answered INVALID/FAIL on data verification')
else:
_logger.warning('Paypal: unrecognized paypal answer, received %s instead of VERIFIED or INVALID' % resp.text)
_logger.warning('Paypal: unrecognized paypal answer, received %s instead of VERIFIED/SUCCESS or INVALID/FAIL (validation: %s)' % (resp, 'PDT' if pdt_request else 'IPN/DPN'))
return res
@http.route('/payment/paypal/ipn/', type='http', auth='none', methods=['POST'])