From 205b5542ca274c2c7dc501b4f30e82e53b45a7bf Mon Sep 17 00:00:00 2001 From: Damien Bouvy Date: Fri, 28 Oct 2016 13:21:04 +0200 Subject: [PATCH] [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. --- addons/payment_paypal/controllers/main.py | 37 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index 7f9ce75986d..f0bd3bb04ae 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -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'])