Files
odoo_source/addons/payment_paypal/controllers/main.py
T
Damien Bouvy 0190b6c492 [FIX] payment_paypal: reply nicely to Paypal IPN with unknown reference
If a Paypal account is configured to send IPN notifications to Odoo at
the account level (as we officially advise in our doc) and is used in
another system than Odoo, transactions from that other system will
trigger notifications that gets sent to Odoo with references that are
unknown.

This tends to generate 500 errors, but Paypal will keep trying to send
the same notification over and over; this will cause a lot of errors in
logs and could also could Paypal to contact the account owner regarding
repeated failure on the IPN endpoints, which is somewhat scary for
lambda users.

This commit simply ensures that any such payment will not trigger a
faulty response (Paypal only accept 200 OK responses), but still keep a
trace of it in the logs as a warning.

Note that enabling IPN at the account level is not strictly necessary,
as transactions sent by Odoo force this behaviour regardless of the
setting with the 'notify_url' param.

closes odoo/odoo#56323

X-original-commit: e4e4b51e0d97c5dd6afe332bf7c70d8321469a52
Signed-off-by: Damien Bouvy (dbo) <dbo@odoo.com>
2020-08-21 15:11:37 +00:00

120 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
import json
import logging
import pprint
import requests
import werkzeug
from werkzeug import urls
from odoo import http
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.http import request
_logger = logging.getLogger(__name__)
class PaypalController(http.Controller):
_notify_url = '/payment/paypal/ipn/'
_return_url = '/payment/paypal/dpn/'
_cancel_url = '/payment/paypal/cancel/'
def _parse_pdt_response(self, response):
""" Parse a text response for a PDT verification.
:param str response: text response, structured in the following way:
STATUS\nkey1=value1\nkey2=value2...\n
or STATUS\nError message...\n
:rtype tuple(str, dict)
:return: tuple containing the STATUS str and the key/value pairs
parsed as a dict
"""
lines = [line for line in response.split('\n') if line]
status = lines.pop(0)
pdt_post = {}
for line in lines:
split = line.split('=', 1)
if len(split) == 2:
pdt_post[split[0]] = urls.url_unquote_plus(split[1])
else:
_logger.warning('Paypal: error processing pdt response: %s', line)
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 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
post['cmd'] = '_notify-validate'
reference = post.get('item_number')
tx = None
if reference:
tx = request.env['payment.transaction'].sudo().search([('reference', '=', reference)])
if not tx:
# we have seemingly received a notification for a payment that did not come from
# odoo, acknowledge it otherwise paypal will keep trying
_logger.warning('received notification for unknown payment reference')
return False
paypal_url = tx.acquirer_id.paypal_get_form_action_url()
pdt_request = bool(post.get('amt')) # check for specific pdt param
if pdt_request:
# this means we are in PDT instead of DPN like before
# fetch the PDT token
post['at'] = tx and tx.acquirer_id.paypal_pdt_token or ''
post['cmd'] = '_notify-synch' # command is different in PDT than IPN/DPN
urequest = requests.post(paypal_url, post)
urequest.raise_for_status()
resp = urequest.text
if pdt_request:
resp, post = self._parse_pdt_response(resp)
if resp in ['VERIFIED', 'SUCCESS']:
_logger.info('Paypal: validated data')
res = request.env['payment.transaction'].sudo().form_feedback(post, 'paypal')
if not res and tx:
tx._set_transaction_error('Validation error occured. Please contact your administrator.')
elif resp in ['INVALID', 'FAIL']:
_logger.warning('Paypal: answered INVALID/FAIL on data verification')
if tx:
tx._set_transaction_error('Invalid response from Paypal. Please contact your administrator.')
else:
_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'))
if tx:
tx._set_transaction_error('Unrecognized error from Paypal. Please contact your administrator.')
return res
@http.route('/payment/paypal/ipn/', type='http', auth='public', methods=['POST'], csrf=False)
def paypal_ipn(self, **post):
""" Paypal IPN. """
_logger.info('Beginning Paypal IPN form_feedback with post data %s', pprint.pformat(post)) # debug
try:
self.paypal_validate_data(**post)
except ValidationError:
_logger.exception('Unable to validate the Paypal payment')
return ''
@http.route('/payment/paypal/dpn', type='http', auth="public", methods=['POST', 'GET'], csrf=False)
def paypal_dpn(self, **post):
""" Paypal DPN """
_logger.info('Beginning Paypal DPN form_feedback with post data %s', pprint.pformat(post)) # debug
try:
res = self.paypal_validate_data(**post)
except ValidationError:
_logger.exception('Unable to validate the Paypal payment')
return werkzeug.utils.redirect('/payment/process')
@http.route('/payment/paypal/cancel', type='http', auth="public", csrf=False)
def paypal_cancel(self, **post):
""" When the user cancels its Paypal payment: GET on this route """
_logger.info('Beginning Paypal cancel with post data %s', pprint.pformat(post)) # debug
return werkzeug.utils.redirect('/payment/process')