taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | Submodules | README | LICENSE

commit 200b658d3152774345a4dbd6adfb10f62581233a
parent 0490626fc0f24506eaa78a4fb72d55988ecd903f
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu,  8 Feb 2024 17:45:16 -0300

fixes #8137

Diffstat:
Mtalermerchantdemos/blog/blog.py | 291+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtalermerchantdemos/httpcommon/__init__.py | 17++++++++++++++++-
2 files changed, 158 insertions(+), 150 deletions(-)

diff --git a/talermerchantdemos/blog/blog.py b/talermerchantdemos/blog/blog.py @@ -40,6 +40,7 @@ from talermerchantdemos.httpcommon import ( backend_get, backend_get_with_status, backend_post, + backend_payment_url, self_localized, Deadline, BackendException, @@ -324,15 +325,16 @@ def render_article(article_name, lang, data, order_id, refundable): # @param article_name which article the order is for # @param lang which language to use # -def post_order(article_name, lang): +def post_order(article_name, article_url, session_id, lang): article_info = ARTICLES[lang].get(article_name) summary = f"Essay: {article_info.title}" order = dict( amount=ARTICLE_AMOUNT, extra=dict(article_name=article_name), - fulfillment_url=flask.request.base_url, - public_reorder_url=flask.request.base_url, + fulfillment_url=article_url, + public_reorder_url=article_url, summary=summary, + session_id=session_id, # FIXME: add support for i18n of summary! # 10 minutes time for a refund wire_transfer_deadline=dict(t_s=int(time.time() + 15 * 30)), @@ -375,169 +377,160 @@ def article(article_name, lang=None, data=None): # the user agent supports cookies. All the key-value pairs associated to it # are only stored in the server. session_id = flask.session.get("session_id") - order_id = flask.request.cookies.get("order_id") - - # Check if cookies are expected for this request. + maybe_expect_state = request.args.get("expect_state") new_if_refunded = request.args.get("new_if_refunded") + current_order_id = flask.request.cookies.get("order_id") + article_url = flask.request.base_url - # Check if cookies are expected for this request. - maybe_expect_state = request.args.get("expect_state") - if maybe_expect_state == "yes": - if not order_id: - error_page = flask.render_template( - "blog-error.html.j2", - page_title=gettext("GNU Taler Demo: Error"), - message=gettext("Please enable cookies."), - ) - return flask.make_response(error_page, 412) - # Cookies enabled and found, redirect once again by - # stripping the "expect_state" parameter. - return flask.redirect(req_rm_cookie_check(), code=302) - - # Whenever we set one session ID, we nullify the order ID (regardless - # of it being found in the cookies or not). if not session_id: - session_id = flask.session["session_id"] = str(uuid.uuid4()) - # This command ensures that fresh sessions are tied to fresh order IDs. - order_id = None - - # Merchant backend wasn't asked already to generate the order under - # this session, so doing it now. - if not order_id: - if not lang: - err_abort(403, message=gettext("Direct access forbidden")) - order_resp = post_order(article_name, lang) - order_id = order_resp["order_id"] - - # Ask the backend for the status of the payment. - status, pay_status = backend_get_with_status( - BACKEND_URL, - f"private/orders/{order_id}", - params=dict(session_id=session_id), - auth_token=APIKEY, - ) - - if status == 404: - order_resp = post_order(article_name, lang) - order_id = order_resp["order_id"] - pay_status = backend_get( - BACKEND_URL, - f"private/orders/{order_id}", - params=dict(session_id=session_id), - auth_token=APIKEY, + # If expect_state = yes then session_id should be set already + # this is a way to check that the client supports cookies + if maybe_expect_state == "yes": + error_page = flask.render_template( + "blog-error.html.j2", + page_title=gettext("GNU Taler Demo: Error"), + message=gettext("Please enable cookies."), ) - elif status != 200: + return flask.make_response(error_page, 412) + + # first time setting session_id + # check if browser support cookies with a flag + session_id = flask.session["session_id"] = str(uuid.uuid4()) + return flask.redirect(req_add_cookie_check(), code=302) + + # If session is present then we know that cookies are enabled + # remove the flag if present + if maybe_expect_state == "yes": + return flask.redirect(req_rm_cookie_check(), code=302) + + ############################ + # user has a session and cookie works + # + # check if we can already render the article + ############################ + + # if an order_id is present then render if paid or refunded + if current_order_id is not None: + status, current_order = backend_get_with_status( + BACKEND_URL, + f"private/orders/{current_order_id}", + params=dict(session_id=session_id), + auth_token=APIKEY, + ) + + if status == 200: + if current_order.get("order_status") == "paid" and not current_order.get("refunded"): + return render_article( + article_name, + lang, + data, + current_order_id, + current_order.get("refundable") + ) + + # Checking repurchase case. That happens when the client + # visits this page in the same session where the article + # was paid already. + # ai = pay_status.get("already_paid_order_id") + # au = pay_status.get("already_paid_fulfillment_url") + + # FIXME: ignoring becuase of https://bugs.gnunet.org/view.php?id=8353 + ai = None + au = None + if ai is not None: + print("== Merchant says 'see other': ", ai, au) + response = flask.redirect(article_url) + response.set_cookie( + "order_id", + order_id, + path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") + ) + response.set_cookie( + "order_id", + order_id, + path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") + ) + return response + + # If new_if_refunded == "yes" the user already acknowledge the + # state of the current order and is asking for a new one. + if current_order.get("refunded") and new_if_refunded != "yes": + return flask.render_template( + "blog-article-refunded.html.j2", + page_title=gettext("GNU Taler Demo: Refunded"), + article_name=article_name, + article_lang=lang, + order_id=current_order_id, + ) + + elif status != 404: + # not found may be normal, could means that + # merchant forgot about the order becuase + # it was a long time without being paid raise BackendException( - message=gettext("Backend returned error status"), - backend_status=status, - backend_json=pay_status, - ) - order_status = pay_status.get("order_status") - - if order_status == "claimed": - if not lang: - err_abort(403, message=gettext("Direct access forbidden")) - # Order already claimed, must setup fresh order - order_resp = post_order(article_name, lang) - order_id = order_resp["order_id"] - pay_status = backend_get( - BACKEND_URL, - f"private/orders/{order_id}", - params=dict(session_id=session_id), - auth_token=APIKEY, - ) - order_status = pay_status.get("order_status") - - if order_status == "paid" and not pay_status["refunded"]: - return render_article( - article_name, - lang, - data, - order_id, - refundable(pay_status) + message=gettext("Backend returned error status"), + backend_status=status, + backend_json=current_order, ) - #If the order is refunded show the status, unless the user is already aware - #and notified using the query parameter to create a new order - if order_status == "paid" and pay_status.get("refunded"): - if new_if_refunded == "yes": - order_resp = post_order(article_name, lang) - order_id = order_resp["order_id"] - pay_status = backend_get( - BACKEND_URL, - f"private/orders/{order_id}", - params=dict(), - auth_token=APIKEY, - ) - # Order ID is fresh, thus setting the cookies now, and - # redirecting the client to the cookie support check. - redirect_url = pay_status["order_status_url"] - response = flask.redirect(redirect_url) - response.set_cookie( - "order_id", - order_id, - path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") - ) - response.set_cookie( - "order_id", - order_id, - path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") - ) - return response - - else: - return flask.render_template( - "blog-article-refunded.html.j2", - page_title=gettext("GNU Taler Demo: Refunded"), - article_name=article_name, - article_lang=lang, - order_id=order_id, - ) + + # Current order is not present or unpaid + # Check if there is a paid but not refunded order in this session + list_resp = backend_get( + BACKEND_URL, + f"private/orders", + params=dict(session_id=session_id, fulfillment_url=article_url, refunded="no"), + auth_token=APIKEY, + ) - # Checking repurchase case. That happens when the client - # visits this page in the same session where the article - # was paid already. - ai = pay_status.get("already_paid_order_id") - au = pay_status.get("already_paid_fulfillment_url") - - # If the condition below holds, then the browser gets - # the paid order ID in the cookies, and the protocol starts - # again from that associated fulfillment URL (likely to be - # this page). - if ai is not None and au is not None: - # NOT appending the "expect_state" URI param to check - # cookies, because at this point the user agent DID show - # a session, so their cookies must be enabled. - response = flask.redirect(au) + already_paid_order = None + for order in list_resp.get("orders"): + if order.get("paid"): + already_paid_order = order + break + + if already_paid_order is not None: + # Found one, now this is the current order. + print("== Already paid order found", already_paid_order) + order_id = already_paid_order.get("order_id") + response = flask.redirect(article_url) response.set_cookie( "order_id", - ai, + order_id, path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") ) response.set_cookie( "order_id", - ai, + order_id, path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") ) return response - - # No claim, nor repurchase, nor paid statuses were found so far. - # The actual payment protocol needs to be run, and the following - # URL instructs the browser+wallet to run it. - redirect_url = pay_status["order_status_url"] - # The order_id cookies MIGHT already be set, if the browser - # passed successfully the cookie support check (see at the very - # top of this function), and got redirected here thereafter. - # In this case, we can now redirect the browser to the URL that - # triggers the wallet. - - if flask.request.cookies.get("order_id"): - LOGGER.info("Redirecting (with order_id cookies) to", redirect_url) - return flask.redirect(redirect_url) - - # Order ID is fresh, thus setting the cookies now, and - # redirecting the client to the cookie support check. - response = flask.redirect(req_add_cookie_check()) + + ############################ + # We couln't find a paid order + # + # Note that it could be the case that the user is still paying + # an order with another device, in other browser on the same + # session or claimed in the same brower. + # Still, creating an order is cheap and we can safely redirect + # to a payment page and relay on repurchase detection to avoid + # double payments. + # + # create a new order and ask for payment + ############################ + + order_resp = post_order(article_name, article_url, session_id, lang) + order_id = order_resp["order_id"] + token = order_resp["token"] + + redirect_url = backend_payment_url( + BACKEND_URL, + f"orders/{order_id}", + session_id, + token + ) + print("new order URL", redirect_url) + response = flask.redirect(redirect_url) response.set_cookie( "order_id", order_id, diff --git a/talermerchantdemos/httpcommon/__init__.py b/talermerchantdemos/httpcommon/__init__.py @@ -1,6 +1,6 @@ import flask import requests -from urllib.parse import urljoin +from urllib.parse import urljoin, urlencode, urlparse, urlunparse from flask import request, url_for from datetime import datetime import time @@ -117,6 +117,21 @@ def get_locale(): return "en" return lang +## +# Construct the payment URL where customer can pay the order +# +# @param backend_url where the backend is located +# @param order_id id of the order already created +# @param session_id session in which the order is going to be paid +# @param token if the order requires a token +# @return the JSON response from the backend, or a error response +# if something unexpected happens. +def backend_payment_url(backend_url, endpoint, session_id, token): + final_url = urljoin(backend_url, endpoint) + query = urlencode({"token":token, "session_id":session_id}) + redirect_url = urlparse(final_url)._replace(query=query) + return urlunparse(redirect_url) + ## # Helper function used inside Jinja2 logic to create a links