donations.py (10962B)
1 ## 2 # This file is part of GNU TALER. 3 # Copyright (C) 2014-2016, 2020 Taler Systems SA 4 # 5 # TALER is free software; you can redistribute it and/or modify it under the 6 # terms of the GNU Lesser General Public License as published by the Free Software 7 # Foundation; either version 2.1, or (at your option) any later version. 8 # 9 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 12 # 13 # You should have received a copy of the GNU Lesser General Public License along with 14 # GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 # 16 # @author Florian Dold 17 # @author Marcello Stanisci 18 # @brief Implementation of a donations site. 19 20 import base64 21 import logging 22 import flask 23 from flask import url_for 24 from flask_babel import Babel 25 from flask_babel import gettext 26 from werkzeug.middleware.proxy_fix import ProxyFix 27 import os 28 import time 29 from ..httpcommon import backend_post, backend_get, make_utility_processor, get_locale 30 from ..appconfig import load_taler_config 31 import sys 32 33 if not sys.version_info.major == 3 and sys.version_info.minor >= 6: 34 print("Python 3.6 or higher is required.") 35 print( 36 "You are using Python {}.{}.".format( 37 sys.version_info.major, sys.version_info.minor 38 ) 39 ) 40 sys.exit(1) 41 42 logger = logging.getLogger(__name__) 43 44 BABEL_TRANSLATION_DIRECTORIES = "../translations" 45 46 app = flask.Flask(__name__, template_folder="../templates", static_folder="../static") 47 app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1) 48 app.debug = True 49 app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") 50 51 app.config.from_object(__name__) 52 53 babel = Babel(app, locale_selector=get_locale) 54 55 config = load_taler_config() 56 57 CURRENCY = config["taler"]["currency"].value_string(required=True) 58 59 backend_urls = {} 60 backend_apikeys = {} 61 62 63 def add_backend(name): 64 backend_urls[name] = config["frontend-demo-donations"][ 65 f"backend_url_{name}" 66 ].value_string(required=True) 67 backend_apikeys[name] = config["frontend-demo-donations"][ 68 f"backend_apikey_{name}" 69 ].value_string(required=True) 70 71 72 add_backend("tor") 73 add_backend("taler") 74 add_backend("gnunet") 75 76 # Use donation authority if configured 77 donau_url = config["frontend-demo-donations"][f"donau_url"].value_string() 78 if donau_url: 79 donau_url = donau_url.strip() 80 if donau_url == "": 81 donau_url = None 82 83 84 logger.info("currency: " + CURRENCY) 85 86 # Add context processor that will make additional variables 87 # and functions available in the template. 88 app.context_processor( 89 make_utility_processor( 90 "donations", os.environ.get("TALER_ENV_URL_MERCHANT_DONATIONS") 91 ) 92 ) 93 94 95 ## 96 # Return a error response to the client. 97 # 98 # @param abort_status_code status code to return along the response. 99 # @param params _kw_ arguments to passed verbatim to the templating engine. 100 def err_abort(abort_status_code, **params): 101 t = flask.render_template("donations-error.html.j2", **params) 102 flask.abort(flask.make_response(t, abort_status_code)) 103 104 105 ## 106 # Issue a GET request to the backend. 107 # 108 # @param endpoint the backend endpoint where to issue the request. 109 # @param params (dict type of) URL parameters to append to the request. 110 # @return the JSON response from the backend, or a error response 111 # if something unexpected happens. 112 def backend_instanced_get(instance, endpoint, params): 113 return backend_get( 114 backend_urls[instance], endpoint, params, auth_token=backend_apikeys[instance] 115 ) 116 117 118 ## 119 # POST a request to the backend, and return a error 120 # response if any error occurs. 121 # 122 # @param endpoint the backend endpoint where to POST 123 # this request. 124 # @param json the POST's body. 125 # @return the backend response (JSON format). 126 def backend_instanced_post(instance, endpoint, json): 127 return backend_post( 128 backend_urls[instance], endpoint, json, auth_token=backend_apikeys[instance] 129 ) 130 131 132 ## 133 # Inspect GET arguments in the look for a parameter. 134 # 135 # @param name the parameter name to lookup. 136 # @return the parameter value, or a error page if not found. 137 def expect_parameter(name): 138 val = flask.request.args.get(name) 139 if not val: 140 return err_abort(400, message=gettext("parameter '{}' required").format(name)) 141 return val 142 143 144 ## 145 # "Fallback" exception handler to capture all the unmanaged errors. 146 # 147 # @param e the Exception object, currently unused. 148 # @return flask-native response object carrying the error message 149 # (and execution stack!). 150 @app.errorhandler(Exception) 151 def internal_error(e): 152 return flask.render_template( 153 "donations-error.html.j2", 154 page_title=gettext("GNU Taler Demo: Error"), 155 message=str(e), 156 ) 157 158 159 ## 160 # Serve the /favicon.ico requests. 161 # 162 # @return the favicon.ico file. 163 @app.route("/favicon.ico") 164 def favicon(): 165 logger.info("will look into: " + os.path.join(app.root_path, "static")) 166 return flask.send_from_directory( 167 os.path.join(app.root_path, "static"), 168 "favicon.ico", 169 mimetype="image/vnd.microsoft.ico", 170 ) 171 172 173 ## 174 # Serve the main index page, redirecting to /<lang>/ 175 # 176 # @return response object of the index page. 177 @app.route("/") 178 def index(): 179 default = "en" 180 translations = [x.language for x in babel.list_translations()] 181 target = flask.request.accept_languages.best_match(translations, default) 182 return flask.redirect(url_for("index") + target + "/", code=302) 183 184 185 ## 186 # Serve the main index page. 187 # 188 # @return response object of the index page. 189 @app.route("/<lang>/") 190 def start(lang): 191 192 # get_locale defaults to english, hence the 193 # condition below happens only when lang is 194 # wrong or unsupported, respond 404. 195 if lang != get_locale(): 196 err_abort( 197 404, 198 message=f"Language {lang} not found", 199 ) 200 201 return flask.render_template( 202 "donations-index.html.j2", 203 page_title=gettext("GNU Taler Demo: Donations"), 204 merchant_currency=CURRENCY, 205 ) 206 207 208 ## 209 # Serve the "/checkout" page. This page lets the 210 # user pick the payment method they want to use, 211 # and finally confirm the donation. 212 # 213 # @return response object for the /checkout page. 214 @app.route("/<lang>/checkout", methods=["GET"]) 215 def checkout(lang): 216 amount = expect_parameter("donation_amount") 217 donation_receiver = expect_parameter("donation_receiver") 218 donation_donor = expect_parameter("donation_donor") 219 return flask.render_template( 220 "donations-checkout.html.j2", 221 page_title=gettext("GNU Taler Demo: Donations checkout"), 222 donation_amount=amount, 223 donation_receiver=donation_receiver, 224 donation_donor=donation_donor, 225 merchant_currency=CURRENCY, 226 ) 227 228 229 ## 230 # Serve the page advising the user about the impossibility 231 # of further processing the payment method they chose. 232 # 233 # @return response object about the mentioned impossibility. 234 @app.route("/<lang>/provider-not-supported") 235 def provider_not_supported(lang): 236 return flask.render_template( 237 "donations-provider-not-supported.html.j2", 238 page_title=gettext("GNU Taler Demo: Donations"), 239 ) 240 241 242 ## 243 # POST the donation request to the backend. In particular, 244 # it uses the "POST /order" API. 245 # 246 # @return response object that will redirect the browser to 247 # the fulfillment URL, where all the pay-logic will 248 # happen. 249 @app.route("/<lang>/donate") 250 def donate(lang): 251 donation_receiver = expect_parameter("donation_receiver") 252 donation_amount = expect_parameter("donation_amount") 253 donation_donor = expect_parameter("donation_donor") 254 payment_system = expect_parameter("payment_system") 255 if payment_system != "taler": 256 return flask.redirect(flask.url_for("provider_not_supported", lang=lang)) 257 fulfillment_url = flask.url_for( 258 "fulfillment", 259 timestamp=str(time.time()), 260 receiver=donation_receiver, 261 lang=lang, 262 _external=True, 263 ) 264 fulfillment_url = fulfillment_url + "&order_id=${ORDER_ID}" 265 if donau_url is None: 266 order = dict( 267 amount=donation_amount, 268 extra=dict( 269 donor=donation_donor, receiver=donation_receiver, amount=donation_amount 270 ), 271 fulfillment_url=fulfillment_url, 272 summary="Donation to {}".format(donation_receiver), 273 wire_transfer_deadline=dict(t_s=int(time.time() + 10)), 274 minimum_age=16, 275 ) 276 else: 277 order = dict( 278 version=1, 279 extra=dict( 280 donor=donation_donor, receiver=donation_receiver, amount=donation_amount 281 ), 282 fulfillment_url=fulfillment_url, 283 summary="Donation to {} (with receipt)".format(donation_receiver), 284 wire_transfer_deadline=dict(t_s=int(time.time() + 10)), 285 minimum_age=16, 286 choices=[ 287 dict( 288 amount=donation_amount, 289 outputs=[dict(type="tax-receipt", donau_urls=[donau_url])], 290 ), 291 ], 292 ) 293 294 order_resp = backend_instanced_post( 295 donation_receiver, "private/orders", dict(order=order) 296 ) 297 298 if not order_resp: 299 return err_abort( 300 500, # FIXME: status code got lost in the httpcommon module. 301 message=gettext("Backend could not create the order"), 302 ) 303 304 order_id = order_resp["order_id"] 305 return flask.redirect( 306 flask.url_for( 307 "fulfillment", receiver=donation_receiver, order_id=order_id, lang=lang 308 ) 309 ) 310 311 312 ## 313 # Serve the fulfillment page. 314 # 315 # @param receiver the donation receiver name, that should 316 # correspond to a merchant instance. 317 # @return after the wallet sent the payment, the final HTML "congrats" 318 # page is returned; otherwise, the browser will be redirected 319 # to a page that accepts the payment. 320 @app.route("/<lang>/donation/<receiver>") 321 def fulfillment(lang, receiver): 322 order_id = expect_parameter("order_id") 323 pay_status = backend_instanced_get( 324 receiver, f"private/orders/{order_id}", params=dict() 325 ) 326 order_status = pay_status.get("order_status") 327 if order_status == "paid": 328 extra = pay_status["contract_terms"]["extra"] 329 return flask.render_template( 330 "donations-fulfillment.html.j2", 331 page_title=gettext("GNU Taler Demo: Donations"), 332 donation_receiver=extra["receiver"], 333 donation_amount=extra["amount"], 334 donation_donor=extra["donor"], 335 order_id=order_id, 336 currency=CURRENCY, 337 ) 338 return flask.redirect(pay_status["order_status_url"]) 339 340 341 @app.errorhandler(404) 342 def handler(e): 343 return flask.render_template( 344 "donations-error.html.j2", 345 page_title=gettext("GNU Taler Demo: Error"), 346 message=gettext("Page not found"), 347 )