##
# This file is part of GNU TALER.
# Copyright (C) 2014-2016, 2020 Taler Systems SA
#
# 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 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 os
import time
import traceback
import urllib
from taler.util.talerconfig import TalerConfig, ConfigurationError
from urllib.parse import urljoin
from ..httpcommon import backend_post, backend_get, make_utility_processor, get_locale
import sys
if not sys.version_info.major == 3 and sys.version_info.minor >= 6:
print("Python 3.6 or higher is required.")
print(
"You are using Python {}.{}.".format(
sys.version_info.major, sys.version_info.minor
)
)
sys.exit(1)
logging.basicConfig()
LOGGER = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
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")
try:
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)
BABEL_TRANSLATION_DIRECTORIES = "../translations"
app.config.from_object(__name__)
babel = Babel(app)
babel.localeselector(get_locale)
LOGGER.info("Using translations from:" + ":".join(list(babel.translation_directories)))
LOGGER.info("backend: " + BACKEND_BASE_URL)
LOGGER.info("currency: " + CURRENCY)
translations = [str(translation) for translation in babel.list_translations()]
if not "en" in translations:
translations.append("en")
LOGGER.info(
"Operating with the following translations available: " + " ".join(translations)
)
# Add context processor that will make additional variables
# and functions available in the template.
app.context_processor(make_utility_processor("donations", os.environ.get ("TALER_ENV_URL_MERCHANT_DONATIONS")))
##
# 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("donations-error.html.j2", **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, auth_token=APIKEY)
##
# 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, auth_token=APIKEY)
##
# 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=gettext("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(
"donations-error.html.j2",
page_title=gettext("GNU Taler Demo: Error"),
message=str(e))
##
# Serve the /favicon.ico requests.
#
# @return the favicon.ico file.
@app.route("/favicon.ico")
def favicon():
LOGGER.info("will look into: " + os.path.join(app.root_path, "static"))
return flask.send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.ico",
)
##
# Serve the main index page, redirecting to //
#
# @return response object of the index page.
@app.route("/")
def index():
default = "en"
target = flask.request.accept_languages.best_match(translations, default)
return flask.redirect(url_for ('index') + target + "/", code=302)
##
# Serve the main index page.
#
# @return response object of the index page.
@app.route("//")
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=f"Language {lang} not found",
)
return flask.render_template(
"donations-index.html.j2",
page_title=gettext("GNU Taler Demo: Donations"),
merchant_currency=CURRENCY
)
##
# 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(lang):
amount = expect_parameter("donation_amount")
donation_receiver = expect_parameter("donation_receiver")
donation_donor = expect_parameter("donation_donor")
return flask.render_template(
"donations-checkout.html.j2",
page_title=gettext("GNU Taler Demo: Donations checkout"),
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(lang):
return flask.render_template(
"donations-provider-not-supported.html.j2",
page_title=gettext("GNU Taler Demo: Donations"),
)
##
# 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(lang):
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", lang=lang))
fulfillment_url = flask.url_for(
"fulfillment",
timestamp=str(time.time()),
receiver=donation_receiver,
lang=lang,
_external=True,
)
fulfillment_url = fulfillment_url + "&order_id=${ORDER_ID}"
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),
wire_transfer_deadline=dict(t_s=int(time.time() + 10)),
minimum_age = 16
)
order_resp = backend_instanced_post(
donation_receiver, "private/orders", dict(order=order)
)
if not order_resp:
return err_abort(
500, # FIXME: status code got lost in the httpcommon module.
message=gettext("Backend could not create the order")
)
order_id = order_resp["order_id"]
return flask.redirect(
flask.url_for(
"fulfillment", receiver=donation_receiver, order_id=order_id, lang=lang
)
)
##
# 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(lang, receiver):
order_id = expect_parameter("order_id")
pay_status = backend_instanced_get(
receiver, f"private/orders/{order_id}", params=dict()
)
order_status = pay_status.get("order_status")
if order_status == "paid":
extra = pay_status["contract_terms"]["extra"]
return flask.render_template(
"donations-fulfillment.html.j2",
page_title=gettext("GNU Taler Demo: Donations"),
donation_receiver=extra["receiver"],
donation_amount=extra["amount"],
donation_donor=extra["donor"],
order_id=order_id,
currency=CURRENCY,
)
return flask.redirect(pay_status["order_status_url"])
@app.errorhandler(404)
def handler(e):
return flask.render_template(
"donations-error.html.j2",
page_title=gettext("GNU Taler Demo: Error"),
message=gettext("Page not found")
)