## # 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 logging import flask import lxml.etree import os import qrcode import qrcode.image.svg import time import traceback import urllib from taler.util.talerconfig import TalerConfig from urllib.parse import urljoin from ..httpcommon import backend_post, backend_get 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}/") return backend_get(backend_url, endpoint, params) ## # 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}/") return backend_post(backend_url, endpoint, 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), refund_deadline=dict(t_ms=1000*int(time.time() + 10 * 30)), wire_transfer_deadline=dict(t_ms=1000*int(time.time() + 15 * 30)) ) order_resp = backend_instanced_post(donation_receiver, "private/orders", 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, f"private/orders/{order_id}", params=dict() ) 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, f"private/orders/{order_id}", params=dict() ) 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 ) 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