Files
odoo_source/addons/payment_paypal/controllers/main.py
T
Denis Ledoux 8ecec2bfbb [FIX] payment_paypal: prevent the IPN feedback to return an HTTP error code
When you enable the IPN notifications in your Paypal account configuration,
the default IPN URL is mandatory.

You are therefore obliged to put the IPN feedback URL of Odoo
in this default URL, even if this is not used because
each transaction done by Odoo specifies the URL to which the IPN
notifications must be sent.

Because of this, if you use your Paypal account
for something else, e.g. Ebay, you receive
notifications of payments for these other transactions,
that have nothing to do with Odoo.

In such a case, Odoo returned an HTTP error status code (e.g. 500),
because Odoo couldn't find the given transactions, and
then Paypal sent emails to the Paypal owner
to tell the IPN feedback was failing. Eventually, after
several fails, Paypal automatically disabled the IPN
notifications of the given account.

With this revision, we now avoid to raise the Validation errors
in the case of the IPN feedback, and just logs it,
to avoid Paypal to sent emails to
the owner and to automatically disable the IPN notifications
if it receives transactions from other services than Odoo.

opw-702403
2017-03-06 10:37:49 +01:00

114 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
import json
import logging
import pprint
import urllib
import urllib2
import werkzeug
from openerp import http, SUPERUSER_ID
from openerp.addons.payment.models.payment_acquirer import ValidationError
from openerp.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 _get_return_url(self, **post):
""" 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 post.pop('cm', False) or '{}')
return_url = custom.get('return_url', '/')
return return_url
def _parse_pdt_response(self, response):
""" Parse a text response 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)
# html unescape
for post in pdt_post:
pdt_post[post] = urllib.unquote_plus(pdt_post[post]).decode('utf8')
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
new_post = dict(post, cmd='_notify-validate')
cr, uid, context = request.cr, request.uid, request.context
reference = post.get('item_number')
tx = None
if reference:
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 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' 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/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'], 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="none", 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
return_url = self._get_return_url(**post)
self.paypal_validate_data(**post)
return werkzeug.utils.redirect(return_url)
@http.route('/payment/paypal/cancel', type='http', auth="none", csrf=False)
def paypal_cancel(self, **post):
""" When the user cancels its Paypal payment: GET on this route """
cr, uid, context = request.cr, SUPERUSER_ID, request.context
_logger.info('Beginning Paypal cancel with post data %s', pprint.pformat(post)) # debug
return_url = self._get_return_url(**post)
return werkzeug.utils.redirect(return_url)