summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcello Stanisci <stanisci.m@gmail.com>2017-11-24 20:00:37 +0100
committerMarcello Stanisci <stanisci.m@gmail.com>2017-11-24 20:00:37 +0100
commit5b29c82bcd05efd43152ef0dd39e1d7f3b457933 (patch)
treefab25b5ecf81384f53c3c32b90284faea1f10d63
parent7b82d72dc249563f38046a6af0e57da6c5d2dde4 (diff)
downloadblog-5b29c82bcd05efd43152ef0dd39e1d7f3b457933.tar.gz
blog-5b29c82bcd05efd43152ef0dd39e1d7f3b457933.tar.bz2
blog-5b29c82bcd05efd43152ef0dd39e1d7f3b457933.zip
actual blog logic
-rw-r--r--talerblog/blog/blog.py248
-rw-r--r--talerblog/blog/content.py131
2 files changed, 379 insertions, 0 deletions
diff --git a/talerblog/blog/blog.py b/talerblog/blog/blog.py
new file mode 100644
index 0000000..02c6c2d
--- /dev/null
+++ b/talerblog/blog/blog.py
@@ -0,0 +1,248 @@
+# This file is part of GNU TALER.
+# Copyright (C) 2014-2017 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 <http://www.gnu.org/licenses/>
+#
+# @author Florian Dold
+# @author Marcello Stanisci
+
+
+"""
+Implement URL handlers and payment logic for the blog merchant.
+"""
+
+import flask
+from urllib.parse import urljoin, urlencode, quote, parse_qsl
+import requests
+import logging
+import os
+import base64
+import random
+import time
+import json
+import datetime
+from pprint import pprint
+from talerfrontends.talerconfig import TalerConfig
+from talerfrontends.helpers import (make_url,
+ expect_parameter, join_urlparts, get_query_string,
+ backend_error)
+from talerfrontends.blog.content import (articles,
+get_article_file, get_image_file)
+
+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_URL = tc["frontends"]["backend"].value_string(required=True)
+CURRENCY = tc["taler"]["currency"].value_string(required=True)
+INSTANCE = tc["blog"]["instance"].value_string(required=True)
+ARTICLE_AMOUNT = dict(value=1, fraction=0, currency=CURRENCY)
+
+app.config.from_object(__name__)
+
+
+@app.context_processor
+def utility_processor():
+ def url(my_url):
+ return join_urlparts(flask.request.script_root, my_url)
+ def env(name, default=None):
+ return os.environ.get(name, default)
+ return dict(url=url, env=env)
+
+
+@app.route("/")
+def index():
+ return flask.render_template("templates/index.html",
+ merchant_currency=CURRENCY,
+ articles=articles.values())
+
+
+@app.route("/javascript")
+def javascript_licensing():
+ return flask.render_template("templates/javascript.html")
+
+# Triggers the refund by serving /refund/test?order_id=XY.
+# Will be triggered by a "refund button".
+@app.route("/refund", methods=["GET", "POST"])
+def refund():
+ if flask.request.method == "POST":
+ payed_articles = flask.session["payed_articles"] = flask.session.get("payed_articles", {})
+ article_name = flask.request.form.get("article_name")
+ if not article_name:
+ return flask.jsonify(dict(error="No article_name found in form")), 400
+ logger.info("Looking for %s to refund" % article_name)
+ order_id = payed_articles.get(article_name)
+ if not order_id:
+ return flask.jsonify(dict(error="Aborting refund: article not payed")), 401
+ r = requests.post(urljoin(BACKEND_URL, "refund"),
+ json=dict(order_id=order_id,
+ refund=dict(value=1, fraction=0, currency=CURRENCY),
+ reason="Demo reimbursement",
+ instance=INSTANCE))
+ if 200 != r.status_code:
+ return backend_error(r)
+ payed_articles[article_name] = "__refunded"
+ response = flask.make_response()
+ response.headers["X-Taler-Refund-Url"] = make_url("/refund", ("order_id", order_id))
+ return response, 402
+
+ else:
+ order_id = expect_parameter("order_id", False)
+ if not order_id:
+ logger.error("Missing parameter 'order_id'")
+ return flask.jsonify(dict(error="Missing parameter 'order_id'")), 400
+ r = requests.get(urljoin(BACKEND_URL, "refund"), params=dict(order_id=order_id,
+ instance=INSTANCE))
+ if 200 != r.status_code:
+ return backend_error(r)
+ return flask.jsonify(r.json()), r.status_code
+
+
+@app.route("/generate-contract", methods=["GET"])
+def generate_contract():
+ article_name = expect_parameter("article_name")
+ pretty_name = article_name.replace("_", " ")
+ order = dict(
+ summary=pretty_name,
+ nonce=flask.request.args.get("nonce"),
+ amount=ARTICLE_AMOUNT,
+ max_fee=dict(value=1, fraction=0, currency=CURRENCY),
+ products=[
+ dict(
+ description="Essay: " + pretty_name,
+ quantity=1,
+ product_id=0,
+ price=ARTICLE_AMOUNT,
+ ),
+ ],
+ fulfillment_url=make_url("/essay/" + quote(article_name)),
+ pay_url=make_url("/pay"),
+ merchant=dict(
+ instance=INSTANCE,
+ address="nowhere",
+ name="Kudos Inc.",
+ jurisdiction="none",
+ ),
+ extra=dict(article_name=article_name),
+ )
+ r = requests.post(urljoin(BACKEND_URL, "proposal"), json=dict(order=order))
+ if r.status_code != 200:
+ return backend_error(r)
+ proposal_resp = r.json()
+ return flask.jsonify(**proposal_resp)
+
+
+@app.route("/cc-payment/<name>")
+def cc_payment(name):
+ return flask.render_template("templates/cc-payment.html",
+ article_name=name)
+
+
+@app.route("/essay/<name>")
+@app.route("/essay/<name>/data/<data>")
+def article(name, data=None):
+ logger.info("processing %s" % name)
+ payed_articles = flask.session.get("payed_articles", {})
+
+ if payed_articles.get(name, "") == "__refunded":
+ return flask.render_template("templates/article_refunded.html", article_name=name)
+
+ if name in payed_articles:
+ article = articles[name]
+ if article is None:
+ flask.abort(500)
+ if data is not None:
+ if data in article.extra_files:
+ return flask.send_file(get_image_file(data))
+ else:
+ return "permission denied", 400
+ return flask.render_template("templates/article_frame.html",
+ article_file=get_article_file(article),
+ article_name=name)
+
+ contract_url = make_url("/generate-contract", ("article_name",name))
+ response = flask.make_response(flask.render_template("templates/fallback.html"), 402)
+ response.headers["X-Taler-Contract-Url"] = contract_url
+ response.headers["X-Taler-Contract-Query"] = "fulfillment_url"
+ # Useless (?) header, as X-Taler-Contract-Url takes always (?) precedence
+ # over X-Offer-Url. This one might only be useful if the contract retrieval
+ # goes wrong.
+ response.headers["X-Taler-Offer-Url"] = make_url("/essay/" + quote(name))
+ return response
+
+
+@app.route("/pay", methods=["POST"])
+def pay():
+ deposit_permission = flask.request.get_json()
+ if deposit_permission is None:
+ e = flask.jsonify(error="no json in body"),
+ return e, 400
+ r = requests.post(urljoin(BACKEND_URL, "pay"), json=deposit_permission)
+ if 200 != r.status_code:
+ return backend_error(r)
+ proposal_data = r.json()["contract_terms"]
+ article_name = proposal_data["extra"]["article_name"]
+ payed_articles = flask.session["payed_articles"] = flask.session.get("payed_articles", {})
+ if len(r.json()["refund_permissions"]) != 0:
+ # we had some refunds on the article purchase already!
+ logger.info("Article %s was refunded, before /pay" % article_name)
+ payed_articles[article_name] = "__refunded"
+ return flask.jsonify(r.json()), 200
+ if not deposit_permission["order_id"]:
+ logger.error("order_id missing from deposit_permission!")
+ return flask.jsonify(dict(error="internal error: ask for refund!")), 500
+ if article_name not in payed_articles:
+ logger.info("Article %s goes in state" % article_name)
+ payed_articles[article_name] = deposit_permission["order_id"]
+ return flask.jsonify(r.json()), 200
+
+
+@app.route("/history")
+def history():
+ qs = get_query_string().decode("utf-8")
+ url = urljoin(BACKEND_URL, "history")
+ r = requests.get(url, params=dict(parse_qsl(qs)))
+ if 200 != r.status_code:
+ return backend_error(r)
+ return flask.jsonify(r.json()), r.status_code
+
+
+@app.route("/backoffice")
+def track():
+ response = flask.make_response(flask.render_template("templates/backoffice.html"))
+ return response
+
+
+@app.route("/track/transfer")
+def track_transfer():
+ qs = get_query_string().decode("utf-8")
+ url = urljoin(BACKEND_URL, "track/transfer")
+ r = requests.get(url, params=dict(parse_qsl(qs)))
+ if 200 != r.status_code:
+ return backend_error(r)
+ return flask.jsonify(r.json()), r.status_code
+
+
+@app.route("/track/order")
+def track_order():
+ qs = get_query_string().decode("utf-8")
+ url = urljoin(BACKEND_URL, "track/transaction")
+ r = requests.get(url, params=dict(parse_qsl(qs)))
+ if 200 != r.status_code:
+ return backend_error(r)
+ return flask.jsonify(r.json()), r.status_code
diff --git a/talerblog/blog/content.py b/talerblog/blog/content.py
new file mode 100644
index 0000000..1c9e98d
--- /dev/null
+++ b/talerblog/blog/content.py
@@ -0,0 +1,131 @@
+# 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 <http://www.gnu.org/licenses/>
+#
+# @author Florian Dold
+
+"""
+Define content and associated metadata that is served on the blog.
+"""
+
+from collections import OrderedDict
+from bs4 import BeautifulSoup
+from pkg_resources import resource_stream, resource_filename
+from collections import namedtuple
+import logging
+import os
+import re
+
+logger = logging.getLogger(__name__)
+
+Article = namedtuple("Article", "slug title teaser main_file extra_files")
+
+articles = OrderedDict()
+
+
+def add_article(slug, title, teaser, main_file, extra_files=[]):
+ articles[slug] = Article(slug, title, teaser, main_file, extra_files)
+
+
+def get_image_file(image):
+ f = resource_filename("talerfrontends", os.path.join("blog/data/", image))
+ return os.path.abspath(f)
+
+
+def get_article_file(article):
+ f = resource_filename("talerfrontends", article.main_file)
+ return os.path.basename(f)
+
+
+def add_from_html(resource_name, teaser_paragraph=0, title=None):
+ """
+ Extract information from article html.
+ """
+ res = resource_stream("talerfrontends", resource_name)
+ soup = BeautifulSoup(res, 'html.parser')
+ if title is None:
+ title_el = soup.find("h1", attrs={"class":["chapter", "unnumbered"]})
+ if title_el is None:
+ logger.warn("Can't extract title from '%s'", resource_name)
+ title = resource_name
+ else:
+ title = title_el.get_text().strip()
+ slug = title.replace(" ", "_")
+ paragraphs = soup.find_all("p")
+
+ teaser = soup.find("p", attrs={"id":["teaser"]})
+ if teaser is None:
+ teaser = str(paragraphs[teaser_paragraph])
+ p = re.compile("^/essay/[^/]+/data/[^/]+$")
+ imgs = soup.find_all("img")
+ extra_files = []
+ for img in imgs:
+ # We require that any image whose access is regulated is src'd
+ # as "<slug>/data/img.png". We also need to check if the <slug>
+ # component actually matches the article's slug
+ if p.match(img['src']):
+ if img['src'].split(os.sep)[2] == slug:
+ logger.info("extra file for %s is %s" % (slug, os.path.basename(img['src'])))
+ extra_files.append(os.path.basename(img['src']))
+ else:
+ logger.warning("Image src and slug don't match: '%s' != '%s'" % (img['src'].split(os.sep)[2], slug))
+ add_article(slug, title, teaser, resource_name, extra_files)
+
+
+add_from_html("blog/articles/scrap1_U.0.html", 0)
+add_from_html("blog/articles/scrap1_U.1.html", 0)
+add_from_html("blog/articles/scrap1_1.html", 1)
+add_from_html("blog/articles/scrap1_2.html")
+add_from_html("blog/articles/scrap1_3.html")
+add_from_html("blog/articles/scrap1_4.html")
+add_from_html("blog/articles/scrap1_5.html")
+add_from_html("blog/articles/scrap1_6.html")
+add_from_html("blog/articles/scrap1_7.html")
+add_from_html("blog/articles/scrap1_8.html")
+add_from_html("blog/articles/scrap1_9.html")
+add_from_html("blog/articles/scrap1_10.html")
+add_from_html("blog/articles/scrap1_11.html")
+add_from_html("blog/articles/scrap1_12.html")
+add_from_html("blog/articles/scrap1_13.html", 1)
+add_from_html("blog/articles/scrap1_14.html")
+add_from_html("blog/articles/scrap1_15.html")
+add_from_html("blog/articles/scrap1_16.html")
+add_from_html("blog/articles/scrap1_17.html")
+add_from_html("blog/articles/scrap1_18.html")
+add_from_html("blog/articles/scrap1_19.html")
+add_from_html("blog/articles/scrap1_20.html", 1)
+add_from_html("blog/articles/scrap1_21.html")
+add_from_html("blog/articles/scrap1_22.html")
+add_from_html("blog/articles/scrap1_23.html")
+add_from_html("blog/articles/scrap1_24.html")
+add_from_html("blog/articles/scrap1_25.html", 1)
+add_from_html("blog/articles/scrap1_26.html", 1)
+add_from_html("blog/articles/scrap1_27.html")
+add_from_html("blog/articles/scrap1_28.html", 1)
+add_from_html("blog/articles/scrap1_29.html")
+add_from_html("blog/articles/scrap1_30.html", 1)
+add_from_html("blog/articles/scrap1_31.html", 1)
+add_from_html("blog/articles/scrap1_32.html")
+add_from_html("blog/articles/scrap1_33.html")
+add_from_html("blog/articles/scrap1_34.html")
+add_from_html("blog/articles/scrap1_35.html")
+add_from_html("blog/articles/scrap1_36.html")
+add_from_html("blog/articles/scrap1_37.html")
+add_from_html("blog/articles/scrap1_38.html")
+add_from_html("blog/articles/scrap1_39.html")
+add_from_html("blog/articles/scrap1_40.html")
+add_from_html("blog/articles/scrap1_41.html")
+add_from_html("blog/articles/scrap1_42.html")
+add_from_html("blog/articles/scrap1_43.html", 2)
+add_from_html("blog/articles/scrap1_46.html", 1)
+add_from_html("blog/articles/scrap1_47.html")