From 3ca29bf465d0419c0915924910b4e81a73ec8cef Mon Sep 17 00:00:00 2001 From: MS Date: Wed, 22 Jul 2020 16:11:30 +0200 Subject: import static files --- talermerchantdemos/donations/donations.py | 314 ++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 talermerchantdemos/donations/donations.py (limited to 'talermerchantdemos/donations/donations.py') diff --git a/talermerchantdemos/donations/donations.py b/talermerchantdemos/donations/donations.py new file mode 100644 index 0000000..c6c03e5 --- /dev/null +++ b/talermerchantdemos/donations/donations.py @@ -0,0 +1,314 @@ +## +# This file is part of GNU TALER. +# Copyright (C) 2014-2016 INRIA +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 2.1, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with +# GNU TALER; see the file COPYING. If not, see +# +# @author Florian Dold +# @author Marcello Stanisci +# @brief Implementation of a donations site. + +import base64 +import flask +import logging +import lxml.etree +import os +import qrcode +import qrcode.image.svg +import random +import requests +import time +import traceback +import urllib +import uuid +from taler.util.talerconfig import TalerConfig +from urllib.parse import urljoin, parse_qsl + +LOGGER = logging.getLogger(__name__) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +app = flask.Flask(__name__, template_folder=BASE_DIR) +app.debug = True +app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8') + +TC = TalerConfig.from_env() +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) + +app.config.from_object(__name__) + + +## +# Extend the templating language with a function +# that fetches values from the environment. +# +# @return the environment-reading function. +@app.context_processor +def utility_processor(): + def env(name, default=None): + return os.environ.get(name, default) + + return dict(env=env) + + +## +# Return a error response to the client. +# +# @param abort_status_code status code to return along the response. +# @param params _kw_ arguments to passed verbatim to the templating engine. +def err_abort(abort_status_code, **params): + t = flask.render_template("templates/error.html", **params) + flask.abort(flask.make_response(t, abort_status_code)) + + +## +# Issue a GET request to the backend. +# +# @param endpoint the backend endpoint where to issue the request. +# @param params (dict type of) URL parameters to append to the request. +# @return the JSON response from the backend, or a error response +# if something unexpected happens. +def backend_instanced_get(instance, endpoint, params): + backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") + headers = {"Authorization": "ApiKey " + APIKEY} + try: + resp = requests.get( + urljoin(backend_url, endpoint), params=params, headers=headers + ) + except requests.ConnectionError: + err_abort(500, message="Could not establish connection to backend") + try: + response_json = resp.json() + except ValueError: + err_abort(500, message="Could not parse response from backend") + if resp.status_code != 200: + err_abort( + 500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code + ) + return response_json + + +## +# POST a request to the backend, and return a error +# response if any error occurs. +# +# @param endpoint the backend endpoint where to POST +# this request. +# @param json the POST's body. +# @return the backend response (JSON format). +def backend_instanced_post(instance, endpoint, json): + backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") + headers = {"Authorization": "ApiKey " + APIKEY} + try: + resp = requests.post( + urljoin(backend_url, endpoint), json=json, headers=headers + ) + except requests.ConnectionError: + err_abort(500, message=f"Could not establish connection to backend (url={url})") + try: + response_json = resp.json() + except ValueError: + err_abort( + 500, + message="Could not parse response from backend", + status_code=resp.status_code + ) + if resp.status_code != 200: + err_abort( + 500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code + ) + return response_json + + +## +# Inspect GET arguments in the look for a parameter. +# +# @param name the parameter name to lookup. +# @return the parameter value, or a error page if not found. +def expect_parameter(name): + val = flask.request.args.get(name) + if not val: + return err_abort(400, message="parameter '{}' required".format(name)) + return val + + +## +# "Fallback" exception handler to capture all the unmanaged errors. +# +# @param e the Exception object, currently unused. +# @return flask-native response object carrying the error message +# (and execution stack!). +@app.errorhandler(Exception) +def internal_error(e): + return flask.render_template( + "templates/error.html", + message="Internal error", + stack=traceback.format_exc() + ) + + +## +# Serve the main index page. +# +# @return response object of the index page. +@app.route("/") +def index(): + return flask.render_template( + "templates/index.html", merchant_currency=CURRENCY + ) + + +## +# Serve the "/javascript" page. +# +# @return response object for the /javascript page. +@app.route("/javascript") +def javascript_licensing(): + return flask.render_template("templates/javascript.html") + + +## +# Serve the "/checkout" page. This page lets the +# user pick the payment method they want to use, +# and finally confirm the donation. +# +# @return response object for the /checkout page. +@app.route("/checkout", methods=["GET"]) +def checkout(): + amount = expect_parameter("donation_amount") + donation_receiver = expect_parameter("donation_receiver") + donation_donor = expect_parameter("donation_donor") + return flask.render_template( + "templates/checkout.html", + donation_amount=amount, + donation_receiver=donation_receiver, + donation_donor=donation_donor, + merchant_currency=CURRENCY + ) + + +## +# Serve the page advising the user about the impossibility +# of further processing the payment method they chose. +# +# @return response object about the mentioned impossibility. +@app.route("/provider-not-supported") +def provider_not_supported(): + return flask.render_template("templates/provider-not-supported.html") + + +## +# POST the donation request to the backend. In particular, +# it uses the "POST /order" API. +# +# @return response object that will redirect the browser to +# the fulfillment URL, where all the pay-logic will +# happen. +@app.route("/donate") +def donate(): + donation_receiver = expect_parameter("donation_receiver") + donation_amount = expect_parameter("donation_amount") + donation_donor = expect_parameter("donation_donor") + payment_system = expect_parameter("payment_system") + if payment_system != "taler": + return flask.redirect(flask.url_for("provider_not_supported")) + fulfillment_url = flask.url_for( + "fulfillment", + timestamp=str(time.time()), + receiver=donation_receiver, + _external=True + ) + order = dict( + amount=donation_amount, + extra=dict( + donor=donation_donor, + receiver=donation_receiver, + amount=donation_amount + ), + fulfillment_url=fulfillment_url, + summary="Donation to {}".format(donation_receiver), + ) + order_resp = backend_instanced_post(donation_receiver, "order", dict(order=order)) + order_id = order_resp["order_id"] + return flask.redirect( + flask.url_for( + "fulfillment", receiver=donation_receiver, order_id=order_id + ) + ) + + +## +# This endpoint is used by the payment request page +# to check if the payment has been completed via the QR code. +@app.route("/check-status//") +def check_status(instance, order_id): + pay_params = dict(order_id=order_id) + pay_status = backend_instanced_get(instance, "check-payment", pay_params) + return flask.jsonify(paid=pay_status["paid"]) + + +def get_qrcode_svg(data): + factory = qrcode.image.svg.SvgImage + img = qrcode.make(data, image_factory=factory) + return lxml.etree.tostring(img.get_image()).decode("utf-8") + + +## +# Serve the fulfillment page. +# +# @param receiver the donation receiver name, that should +# correspond to a merchant instance. +# @return after the wallet sent the payment, the final HTML "congrats" +# page is returned; otherwise, the browser will be redirected +# to a page that accepts the payment. +@app.route("/donation/") +def fulfillment(receiver): + order_id = expect_parameter("order_id") + pay_params = dict(order_id=order_id) + pay_status = backend_instanced_get(receiver, "check-payment", pay_params) + + if pay_status.get("paid"): + extra = pay_status["contract_terms"]["extra"] + return flask.render_template( + "templates/fulfillment.html", + donation_receiver=extra["receiver"], + donation_amount=extra["amount"], + donation_donor=extra["donor"], + order_id=order_id, + currency=CURRENCY + ) + else: + taler_pay_uri = pay_status["taler_pay_uri"] + qrcode_svg = get_qrcode_svg(taler_pay_uri) + check_status_url_enc = urllib.parse.quote( + flask.url_for("check_status", instance=receiver, order_id=order_id) + ) + content = flask.render_template( + "templates/request_payment.html", + taler_pay_uri=taler_pay_uri, + qrcode_svg=qrcode_svg, + check_status_url_enc=check_status_url_enc + ) + headers = {"Taler": taler_pay_uri} + resp = flask.Response(content, status=402, headers=headers) + return resp + + # no pay_redirect but article not paid, this should never happen! + err_abort(500, message="Internal error, invariant failed", json=pay_status) -- cgit v1.2.3