This branch adds request.redirect on all requests. In case of a front end request, we do an url_for to the location. We removed redirect_with_hash that was only for retro compatibility local_redirect has been renamed to redirect_query, and param keep_hash has been removed and moved. Default code for redirect is 303 now instead of 302. Now redirect and redirect_query make local redirect by default, you need to pass local=False to make external redirect. All werkeug.utils.redirect has been replaced by request.redirect. Http.redirect now use an http.Response type, and it become easy to add an override like 'set_cookies' e.g. Dispatch of a website.page return an http.response too, so we first need to check if it is a cached version before to check if it is an Odoo Response. Migrate your code: http.redirect -> request.redirect(location, code, local) http.local_redirect -> request.redirect_query(location, query, code, local) http.redirect_with_hash -> request.redirect Courtesy of odony for help and review ;) closes odoo/odoo#72599 Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
174 lines
7.9 KiB
Python
174 lines
7.9 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import pprint
|
|
from datetime import datetime
|
|
|
|
import werkzeug
|
|
|
|
from odoo import http
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.http import request
|
|
from odoo.tools import consteq
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class StripeController(http.Controller):
|
|
_checkout_return_url = '/payment/stripe/checkout_return'
|
|
_validation_return_url = '/payment/stripe/validation_return'
|
|
WEBHOOK_AGE_TOLERANCE = 10*60 # seconds
|
|
|
|
@http.route(_checkout_return_url, type='http', auth='public', csrf=False)
|
|
def stripe_return_from_checkout(self, **data):
|
|
""" Process the data returned by Stripe after redirection for checkout.
|
|
|
|
:param dict data: The GET params appended to the URL in `_stripe_create_checkout_session`
|
|
"""
|
|
# Retrieve the tx and acquirer based on the tx reference included in the return url
|
|
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
|
|
'stripe', data
|
|
)
|
|
acquirer_sudo = tx_sudo.acquirer_id
|
|
|
|
# Fetch the PaymentIntent, Charge and PaymentMethod objects from Stripe
|
|
payment_intent = acquirer_sudo._stripe_make_request(
|
|
f'payment_intents/{tx_sudo.stripe_payment_intent}', method='GET'
|
|
)
|
|
_logger.info("received payment_intents response:\n%s", pprint.pformat(payment_intent))
|
|
self._include_payment_intent_in_feedback_data(payment_intent, data)
|
|
|
|
# Handle the feedback data crafted with Stripe API objects
|
|
request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data)
|
|
|
|
# Redirect the user to the status page
|
|
return request.redirect('/payment/status')
|
|
|
|
@http.route(_validation_return_url, type='http', auth='public', csrf=False)
|
|
def stripe_return_from_validation(self, **data):
|
|
""" Process the data returned by Stripe after redirection for validation.
|
|
|
|
:param dict data: The GET params appended to the URL in `_stripe_create_checkout_session`
|
|
"""
|
|
# Retrieve the acquirer based on the tx reference included in the return url
|
|
acquirer_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
|
|
'stripe', data
|
|
).acquirer_id
|
|
|
|
# Fetch the Session, SetupIntent and PaymentMethod objects from Stripe
|
|
checkout_session = acquirer_sudo._stripe_make_request(
|
|
f'checkout/sessions/{data.get("checkout_session_id")}',
|
|
payload={'expand[]': 'setup_intent.payment_method'}, # Expand all required objects
|
|
method='GET'
|
|
)
|
|
_logger.info("received checkout/session response:\n%s", pprint.pformat(checkout_session))
|
|
self._include_setup_intent_in_feedback_data(checkout_session.get('setup_intent', {}), data)
|
|
|
|
# Handle the feedback data crafted with Stripe API objects
|
|
request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data)
|
|
|
|
# Redirect the user to the status page
|
|
return request.redirect('/payment/status')
|
|
|
|
@http.route('/payment/stripe/webhook', type='json', auth='public')
|
|
def stripe_webhook(self):
|
|
""" Process the `checkout.session.completed` event sent by Stripe to the webhook.
|
|
|
|
:return: An empty string to acknowledge the notification with an HTTP 200 response
|
|
:rtype: str
|
|
"""
|
|
event = json.loads(request.httprequest.data)
|
|
_logger.info("event received:\n%s", pprint.pformat(event))
|
|
if event['type'] == 'checkout.session.completed':
|
|
checkout_session = event['data']['object']
|
|
|
|
# Check the source and integrity of the event
|
|
data = {'reference': checkout_session['client_reference_id']}
|
|
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
|
|
'stripe', data
|
|
)
|
|
if self._verify_webhook_signature(tx_sudo.acquirer_id.stripe_webhook_secret):
|
|
if checkout_session.get('payment_intent'): # Can be None
|
|
# Fetch the PaymentIntent, Charge and PaymentMethod objects from Stripe
|
|
payment_intent = tx_sudo.acquirer_id._stripe_make_request(
|
|
f'payment_intents/{tx_sudo.stripe_payment_intent}', method='GET'
|
|
)
|
|
_logger.info(
|
|
"received payment_intents response:\n%s", pprint.pformat(payment_intent)
|
|
)
|
|
self._include_payment_intent_in_feedback_data(payment_intent, data)
|
|
if checkout_session.get('setup_intent'): # Can be None
|
|
# Fetch the SetupIntent and PaymentMethod objects from Stripe
|
|
setup_intent = tx_sudo.acquirer_id._stripe_make_request(
|
|
f'setup_intents/{checkout_session.get("setup_intent")}',
|
|
payload={'expand[]': 'payment_method'},
|
|
method='GET'
|
|
)
|
|
_logger.info(
|
|
"received setup_intents response:\n%s", pprint.pformat(setup_intent)
|
|
)
|
|
self._include_setup_intent_in_feedback_data(setup_intent, data)
|
|
try:
|
|
# Handle the feedback data crafted with Stripe API objects as a regular feedback
|
|
request.env['payment.transaction'].sudo()._handle_feedback_data('stripe', data)
|
|
except ValidationError: # Acknowledge the notification to avoid getting spammed
|
|
_logger.exception("unable to handle the event data; skipping to acknowledge")
|
|
return ''
|
|
|
|
@staticmethod
|
|
def _include_payment_intent_in_feedback_data(payment_intent, data):
|
|
data.update({'payment_intent': payment_intent})
|
|
if payment_intent.get('charges', {}).get('total_count', 0) > 0:
|
|
charge = payment_intent['charges']['data'][0] # Use the latest charge object
|
|
data.update({
|
|
'charge': charge,
|
|
'payment_method': charge.get('payment_method_details'),
|
|
})
|
|
|
|
@staticmethod
|
|
def _include_setup_intent_in_feedback_data(setup_intent, data):
|
|
data.update({
|
|
'setup_intent': setup_intent,
|
|
'payment_method': setup_intent.get('payment_method')
|
|
})
|
|
|
|
def _verify_webhook_signature(self, webhook_secret):
|
|
""" Check that the signature computed from the feedback matches the received one.
|
|
|
|
See https://stripe.com/docs/webhooks/signatures#verify-manually.
|
|
|
|
:param str webhook_secret: The secret webhook key of the acquirer handling the transaction
|
|
:return: Whether the signatures match
|
|
:rtype: str
|
|
"""
|
|
if not webhook_secret:
|
|
_logger.warning("ignored webhook event due to undefined webhook secret")
|
|
return False
|
|
|
|
notification_payload = request.httprequest.data.decode('utf-8')
|
|
signature_entries = request.httprequest.headers.get('Stripe-Signature').split(',')
|
|
signature_data = {k: v for k, v in [entry.split('=') for entry in signature_entries]}
|
|
|
|
# Check the timestamp of the event
|
|
event_timestamp = int(signature_data['t'])
|
|
if datetime.utcnow().timestamp() - event_timestamp > self.WEBHOOK_AGE_TOLERANCE:
|
|
_logger.warning("ignored webhook event due to age tolerance: %s", event_timestamp)
|
|
return False
|
|
|
|
# Compare signatures
|
|
received_signature = signature_data['v1']
|
|
signed_payload = f'{event_timestamp}.{notification_payload}'
|
|
expected_signature = hmac.new(
|
|
webhook_secret.encode('utf-8'),
|
|
signed_payload.encode('utf-8'),
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
if not consteq(received_signature, expected_signature):
|
|
_logger.warning("ignored event with invalid signature")
|
|
return False
|
|
|
|
return True
|