summaryrefslogtreecommitdiff
path: root/talermerchantdemos/blog/blog.py
diff options
context:
space:
mode:
Diffstat (limited to 'talermerchantdemos/blog/blog.py')
-rw-r--r--talermerchantdemos/blog/blog.py297
1 files changed, 221 insertions, 76 deletions
diff --git a/talermerchantdemos/blog/blog.py b/talermerchantdemos/blog/blog.py
index 9722206..bcd1bb8 100644
--- a/talermerchantdemos/blog/blog.py
+++ b/talermerchantdemos/blog/blog.py
@@ -24,19 +24,23 @@ import traceback
import uuid
import base64
import flask
-from flask import request
+import uwsgi
+from flask import request, url_for
from flask_babel import Babel
from flask_babel import refresh
from flask_babel import force_locale
from flask_babel import gettext
+from werkzeug.middleware.proxy_fix import ProxyFix
import time
import sys
from urllib.parse import urljoin, urlencode, urlparse
-from taler.util.talerconfig import TalerConfig, ConfigurationError
+from ..util.talerconfig import TalerConfig, ConfigurationError
from ..blog.content import ARTICLES, get_article_file, get_image_file
from talermerchantdemos.httpcommon import (
backend_get,
+ backend_get_with_status,
backend_post,
+ backend_payment_url,
self_localized,
Deadline,
BackendException,
@@ -44,6 +48,25 @@ from talermerchantdemos.httpcommon import (
get_locale,
)
+def req_add_cookie_check():
+ current_url = list(urllib.parse.urlparse(flask.request.base_url))
+ args_writable = flask.request.args.copy()
+ # Adding the used param.
+ args_writable.update(dict(expect_state="yes"))
+ current_url[4] = urllib.parse.urlencode(args_writable)
+ # Stringify the result.
+ return urllib.parse.urlunparse(current_url)
+
+
+def req_rm_cookie_check():
+ current_url = list(urllib.parse.urlparse(flask.request.base_url))
+ args_writable = flask.request.args.copy()
+ # Stripping the used param.
+ args_writable.pop("expect_state")
+ current_url[4] = urllib.parse.urlencode(args_writable)
+ # Stringify the result.
+ return urllib.parse.urlunparse(current_url)
+
def err_abort(abort_status_code, **params):
"""
@@ -60,14 +83,14 @@ def refundable(pay_status):
refunded = pay_status.get("refunded")
refund_deadline = pay_status.get("contract_terms", {}).get("refund_deadline")
assert refunded != None and refund_deadline
- t_ms = refund_deadline.get("t_ms")
- assert t_ms
- rd = Deadline(t_ms)
+ t_s = refund_deadline.get("t_s")
+ # FIXME: do not use assert here!
+ assert t_s
+ rd = Deadline(t_s * 1000)
if not refunded and not rd.isExpired():
return True
return False
-
if not sys.version_info.major == 3 and sys.version_info.minor >= 6:
print("Python 3.6 or higher is required.")
print(
@@ -78,15 +101,17 @@ if not sys.version_info.major == 3 and sys.version_info.minor >= 6:
sys.exit(1)
app = flask.Flask(__name__, template_folder="../templates", static_folder="../static")
+app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1)
app.debug = True
app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8")
+logging.basicConfig()
LOGGER = logging.getLogger(__name__)
-TC = TalerConfig.from_env()
try:
- BACKEND_BASE_URL = TC["frontends"]["backend"].value_string(required=True)
- CURRENCY = TC["taler"]["currency"].value_string(required=True)
- APIKEY = TC["frontends"]["backend_apikey"].value_string(required=True)
+
+ BACKEND_BASE_URL = uwsgi.opt["backend_url"].decode("utf-8")
+ CURRENCY = uwsgi.opt["currency"].decode("utf-8")
+ APIKEY = uwsgi.opt["apikey"].decode("utf-8")
except ConfigurationError as ce:
print(ce)
exit(1)
@@ -110,7 +135,7 @@ LOGGER.info(
# Add context processor that will make additional variables
# and functions available in the template.
-app.context_processor(make_utility_processor("blog"))
+app.context_processor(make_utility_processor("blog", os.environ.get ("TALER_ENV_URL_MERCHANT_BLOG")))
##
@@ -123,6 +148,7 @@ app.context_processor(make_utility_processor("blog"))
def internal_error(e):
return flask.render_template(
"blog-error.html.j2",
+ page_title=gettext("GNU Taler Demo: Error"),
message=gettext("Internal error"),
stack=traceback.format_exc(),
)
@@ -136,7 +162,7 @@ def internal_error(e):
def index():
default = "en"
target = flask.request.accept_languages.best_match(translations, default)
- return flask.redirect("/" + target + "/", code=302)
+ return flask.redirect(url_for ('index') + target + "/", code=302)
##
@@ -159,6 +185,16 @@ def favicon():
# @return response object of the index page.
@app.route("/<lang>/")
def start(lang):
+
+ # get_locale defaults to english, hence the
+ # condition below happens only when lang is
+ # wrong or unsupported, respond 404.
+ if lang != get_locale():
+ err_abort(
+ 404,
+ message="Language {} not found".format(lang),
+ )
+
if lang in ARTICLES:
translated = ARTICLES[lang]
else:
@@ -166,6 +202,7 @@ def start(lang):
return flask.render_template(
"blog-index.html.j2",
merchant_currency=CURRENCY,
+ page_title=gettext("GNU Taler Demo: Essay Shop"),
articles=translated.values(),
)
@@ -190,10 +227,12 @@ def confirm_refund(lang, order_id):
if not refundable(pay_status):
return flask.render_template(
"blog-error.html.j2",
+ page_title=gettext("GNU Taler Demo: Error"),
message=gettext("Article is not anymore refundable"),
)
return flask.render_template(
"blog-confirm-refund.html.j2",
+ page_title=gettext("GNU Taler Demo: Confirm refund"),
article_name=article_name,
order_id=order_id,
)
@@ -272,8 +311,9 @@ def render_article(article_name, lang, data, order_id, refundable):
article_contents = open(get_article_file(article_info)).read()
return flask.render_template(
"blog-article-frame.html.j2",
+ page_title=gettext("GNU Taler Demo: Article"),
article_contents=article_contents,
- article_name=article_name,
+ article_name=article_info.title,
order_id=order_id,
refundable=refundable,
)
@@ -285,22 +325,24 @@ 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):
- name_decoded = urllib.parse.unquote(article_name).replace("_", " ")
- summary = f"Essay: {name_decoded}"
+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,
+ 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_ms=1000 * int(time.time() + 15 * 30)),
+ wire_transfer_deadline=dict(t_s=int(time.time() + 15 * 30)),
)
order_resp = backend_post(
BACKEND_URL,
"private/orders",
- dict(order=order, refund_delay=dict(d_ms=1000 * 120)),
+ dict(order=order, refund_delay=dict(d_us=1000 * 1000 * 120)),
auth_token=APIKEY,
)
return order_resp
@@ -331,76 +373,176 @@ def post_order(article_name, lang):
def article(article_name, lang=None, data=None):
# We use an explicit session ID so that each payment (or payment replay) is
# bound to a browser. This forces re-play and prevents sharing the article
- # by just sharing the URL.
+ # by just sharing the URL. flask.session is transparently set by Flask, when
+ # 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")
+ maybe_expect_state = request.args.get("expect_state")
+
+ # Disabled for now, see https://bugs.gnunet.org/view.php?id=8137
+ # and https://bugs.gnunet.org/view.php?id=8353
+ new_if_refunded = request.args.get("new_if_refunded")
+
+ current_order_id = flask.request.cookies.get("order_id")
+ article_url = flask.request.base_url
if not session_id:
- session_id = flask.session["session_id"] = str(uuid.uuid4())
- order_id = None
- ##
- # First-timer; generate order first.
- 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"]
+ # 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."),
+ )
+ 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,
+ refundable(current_order)
+ )
+
+ # Checking repurchase case. That happens when the client
+ # visits this page in the same session where the article
+ # was paid already.
+ ai = current_order.get("already_paid_order_id")
+ au = current_order.get("already_paid_fulfillment_url")
+
+ if ai is not None:
+ print("== Merchant says 'see other': ", ai, au)
+ response = flask.redirect(article_url)
+ response.set_cookie(
+ "order_id",
+ ai,
+ path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}")
+ )
+ response.set_cookie(
+ "order_id",
+ ai,
+ 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=current_order,
+ )
- # Ask the backend for the status of the payment
- pay_status = backend_get(
+
+ # 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/{order_id}",
- params=dict(session_id=session_id),
+ f"private/orders",
+ params=dict(session_id=session_id, fulfillment_url=article_url, refunded="no"),
auth_token=APIKEY,
)
- 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")
- # This really must be 'unpaid' now...
-
- if order_status == "paid":
- refunded = pay_status["refunded"]
- if refunded:
- return flask.render_template(
- "blog-article-refunded.html.j2",
- article_name=article_name,
- order_id=order_id,
- )
- response = render_article(
- article_name, lang, data, order_id, refundable(pay_status)
- )
- return response
-
- # Check if the customer came on this page via the
- # re-purchase detection mechanism
- ai = pay_status.get("already_paid_order_id")
- au = pay_status.get("already_paid_fulfillment_url")
- if ai is not None and au is not None:
- 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, path=urllib.parse.quote(f"/essay/{article_name}")
+ "order_id",
+ order_id,
+ path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}")
)
response.set_cookie(
- "order_id", ai, path=urllib.parse.quote(f"/{lang}/essay/{article_name}")
+ "order_id",
+ order_id,
+ path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}")
)
return response
+
+ ############################
+ # 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,
+ 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}")
+ )
- # Redirect the browser to a page where the wallet can
- # run the payment protocol.
- response = flask.redirect(pay_status["order_status_url"])
- response.set_cookie("order_id", order_id, path=f"/essay/{article_name}")
- response.set_cookie("order_id", order_id, path=f"/{lang}/essay/{article_name}")
return response
@@ -408,6 +550,7 @@ def article(article_name, lang=None, data=None):
def handler_500(e):
return flask.render_template(
"blog-error.html.j2",
+ page_title=gettext("GNU Taler Demo: Error"),
message=gettext("Internal server error"),
)
@@ -416,6 +559,7 @@ def handler_500(e):
def handler_404(e):
return flask.render_template(
"blog-error.html.j2",
+ page_title=gettext("GNU Taler Demo: Error"),
message=gettext("Page not found"),
)
@@ -425,6 +569,7 @@ def handler_backend_exception(e):
t = flask.render_template(
"blog-error.html.j2",
message=e.args[0],
+ page_title=gettext("GNU Taler Demo: Error"),
json=e.backend_json,
status_code=e.backend_status,
)