taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit 38c6e180c261cb17311c023c69bf45673ffac871
parent 8dff335c269adf4b4d7fc0c9ea5e75f098f0801e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon, 25 May 2026 22:38:34 +0200

add drupal-turnstile manual

Diffstat:
Mcore/api-merchant.rst | 4++--
Ddrupal-paivana-manual.rst | 27---------------------------
Afrags/drupal-paivana-manual.rst | 542+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rpaivana-httpd-manual.rst -> frags/paivana-httpd-manual.rst | 0
Rwordpress-paivana-manual.rst -> frags/wordpress-paivana-manual.rst | 0
Ascreenshots/drupal-turnstile-module-enable.png | 0
Ascreenshots/drupal-turnstile-node-edit.png | 0
Ascreenshots/drupal-turnstile-paywall-page.png | 0
Ascreenshots/drupal-turnstile-price-category-edit.png | 0
Ascreenshots/drupal-turnstile-price-category-list.png | 0
Ascreenshots/drupal-turnstile-settings-form.png | 0
Ascreenshots/drupal-turnstile-subscription-prices.png | 0
Ascreenshots/taler-turnstile-price-categories.png | 0
Mtaler-paivana-manual.rst | 9+++------
14 files changed, 547 insertions(+), 35 deletions(-)

diff --git a/core/api-merchant.rst b/core/api-merchant.rst @@ -1317,13 +1317,13 @@ and basically present (or optional) all the time. // legal requirements. minimum_age?: Integer; - // Default money pot to use for this product, applies to the + // Default money pot to use for this order, applies to the // amount remaining that was not claimed by money pots of // products or taxes. Not useful to wallets, only for // merchant-internal accounting. If not given, the remaining // account is simply not accounted for in any money pot. // Since **v25**. - default_money_pot?: Integer; + order_default_money_pot?: Integer; // Latest time until which the good or service specified in the // contract may be picked up by the customer. This is usually diff --git a/drupal-paivana-manual.rst b/drupal-paivana-manual.rst @@ -1,27 +0,0 @@ -.. - This file is part of GNU TALER. - - Copyright (C) 2026 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - - @author Christian Grothoff - -.. _drupal-paivana: - -Drupal integration -================== - -This chapter documents the installation and operation of the -Drupal Paivana module. - -FIXME! diff --git a/frags/drupal-paivana-manual.rst b/frags/drupal-paivana-manual.rst @@ -0,0 +1,542 @@ +.. + This file is part of GNU TALER. + + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + @author Christian Grothoff + +.. _drupal-paivana: + + +Drupal integration +================== + +This chapter documents the installation and operation of the +``taler_turnstile`` Drupal module. The module gates individual +nodes (articles, pages, …) of a Drupal 9 or 10 site behind a +GNU Taler paywall: visitors who have not yet paid are shown a +teaser plus a payment button, and once their wallet has paid the +associated merchant template the full content is unlocked. + +The module is designed around the same Paivana-style flow as +:ref:`paivana-httpd <Paivana-httpd>`: a static, cacheable paywall +page references a *payment template* in the merchant backend, no +order is created when a visitor merely loads a paywalled article, +and no PHP session is started for unauthenticated traffic. This +keeps bot traffic from polluting the merchant backend and lets +Drupal's page caches serve the paywall page to anonymous visitors +without per-request work. + + +Architecture overview +--------------------- + +A Drupal site using Turnstile combines three independent +components: + +1. **The Drupal site.** An ordinary Drupal 9 or 10 installation + serving content of one or more content types + (``article``, ``page``, …). The ``taler_turnstile`` module + adds an entity-reference field called *Price category* to the + content types you select, and uses ``hook_entity_view_alter()`` + to intercept rendering of nodes that carry a non-empty value. +2. **A GNU Taler merchant backend** (``taler-merchant-httpd``). + The backend stores one *payment template* per price category + defined in Drupal, manages orders, talks to one or more Taler + exchanges, and ultimately reports back whether a given order + has been paid. See the + :ref:`Taler Merchant Backend Operator Manual + <taler-merchant-backend-operator-manual>` for full details. +3. **The visitor's GNU Taler wallet.** The wallet scans the QR + code shown on the paywall page (or follows the + ``taler://pay-template/…`` link), pays the merchant template + and triggers the unlock. + +Turnstile itself stores no per-visitor state in the Drupal +database; once the merchant backend confirms a payment, Turnstile +mints a short-lived HMAC cookie (``taler_turnstile_paivana``) +that the browser presents on subsequent requests to the same +fulfillment URL. Clearing cookies forgets paid access, but +GNU Taler wallet's repurchase detection will restore the cookie +if the user attempts to pay for the same article again. + + .. note:: + + This feature likely does not interact well with expiration + periods for article purchases. This is a bug that will + require further work to fully address. See #11443. + + +Installation +------------ + +Requirements +^^^^^^^^^^^^ + +- Drupal 9 or 10 +- PHP 8.1 or later (the module uses native enums) +- The Drupal core modules ``node``, ``field``, ``user`` and + ``path_alias`` (the latter is used by the confirmation + endpoint to map fulfillment URLs back to nodes) +- A reachable GNU Taler merchant backend supporting the v29 (or + newer) ``paivana`` template type and the public + ``/sessions/$SESSION_ID`` endpoint +- An access token for the merchant instance with the + ``templates-write`` and ``orders-read`` permissions + + +Obtaining the module +^^^^^^^^^^^^^^^^^^^^ + +The module sources are available from the GNU Taler Git repository as well as +Drupal's own module repository. Download the latest release and extract it +into your Drupal installation's ``modules/custom/`` directory so that the +resulting layout is: + +.. code-block:: shell-session + + $ ls modules/custom/taler_turnstile/ + config/ js/ taler_turnstile.info.yml + src/ templates/ taler_turnstile.module + ... + +There is no build step and no Composer package: the module is +plain PHP and a couple of JavaScript files (the QR code library +is vendored from `davidshimjs/qrcodejs +<https://github.com/davidshimjs/qrcodejs>`__). + + +Enabling the module +^^^^^^^^^^^^^^^^^^^ + +The module can be enabled via Drush or via the Drupal admin +interface: + +.. tab-set:: + + .. tab-item:: Drush + + .. code-block:: shell-session + + $ drush en taler_turnstile + $ drush cr + + .. tab-item:: Admin UI + + Navigate to ``/admin/modules``, locate *GNU Taler Turnstile* + in the *System* package, tick its checkbox and press + *Install*. + + .. image:: screenshots/drupal-turnstile-module-enable.png + + After installation, clear the cache so that Drupal picks + up the new routes, services and theme template + (``drush cr`` or *Configuration → Performance → + Clear all caches*). + +Enabling the module triggers ``hook_install()``, which attaches the +``field_taler_turnstile_prcat`` entity-reference field to every content type +listed in ``enabled_content_types`` (default: ``article``). + +Uninstalling with ``drush pmu taler_turnstile`` removes the +price-category field from every configured content type and +deletes the module's configuration. It does **not** delete +templates from the merchant backend; remove those manually if +desired (see :ref:`template`). + + +Configuring the basics +---------------------- + +The main settings page lives at +``/admin/config/system/taler-turnstile`` and is also reachable +through *Configuration → System → GNU Taler Turnstile basics*. +It collects the four pieces of information Turnstile needs to +talk to the merchant backend and to know which content to gate: + +.. image:: screenshots/drupal-turnstile-settings-form.png + +The fields are: + +- **Enabled content types** — a list of checkboxes derived from + the site's configured node bundles. Each ticked content type + gains the *Price category* field (and gets it removed again + when unticked). The change takes effect immediately on save: + the field is created or destroyed as a side-effect of saving + this form, not via ``drush cim``. +- **Payment backend URL** — the HTTP(S) base URL of the merchant + backend. Instance-specific URLs ending in ``/instances/$ID/`` + are accepted; the URL **must end with a trailing slash**. When + you save the form with a non-empty value, Turnstile contacts the + backend's ``/config`` endpoint to verify reachability before + accepting the value. +- **Access token** — the Bearer token used for every call to the + backend. It **must begin with** ``secret-token:`` (following + RFC 8959, see :ref:`Access control + <taler-merchant-backend-operator-manual>` in the merchant + manual). When both URL and token are present, the form + performs a live authentication check against the backend + and an error is shown if the access token is incorrect. +- **Disable Turnstile when payment backend is unavailable** + (fail-open toggle, on by default) — when the merchant backend + cannot be reached, this controls whether visitors see the + unpaywalled article (toggle on) or an error message in place + of the payment button (toggle off). The settings form refuses + to save with the toggle off and an empty backend URL or token + unless you confirm the warning, so a misconfiguration cannot + silently break the live site. + +The first time you save a working backend URL and access token, +Turnstile pushes a ``paivana``-style template to the merchant +backend for every price category that already exists. Each +template carries the ID ``turnstile-{category_id}``. You do not +need to create those templates by hand, and you do not need to +visit the merchant backend's SPA to manage them — Turnstile keeps +them in sync as you edit the categories (see +:ref:`drupal-paivana-price-categories` below). + +Before continuing, make sure that the merchant backend instance +itself is ready to take payments: + +- The instance has at least one configured + :ref:`bank account <instance-bank-account>`. +- Any legitimization required by the chosen payment service + provider has been completed. + + +.. _drupal-paivana-subscription-prices: + +Configuring subscription prices +------------------------------- + +.. note:: + + The term *subscription* is slightly misleading, as it is not + auto-renewing. We are in the process of renaming this to *passes* which is + clearer. However, the current module uses subscription and thus so does this + manual. + +Turnstile supports the GNU Taler subscription model, in which a +visitor buys a *subscription token* once and the token is then +consumed to unlock individual articles for some validity period. +The catalogue of available *token families* is configured on the +merchant backend (see the merchant manual section on +:ref:`templates <template>` and the wider Taler concept of +subscription tokens); Turnstile reads the catalogue live from +``private/tokenfamilies`` and asks you to set the +*purchase price* per token family per currency. + +Navigate to +``/admin/config/system/taler_turnstile/subscription-prices`` +(also reachable as *Configuration → System → GNU Taler Turnstile +subscription prices*). The form lists one collapsible *Details* +block per token family advertised by the backend, with one +numeric input per supported currency: + +.. image:: screenshots/drupal-turnstile-subscription-prices.png + +Leaving a field empty disables the *Buy subscription in +{currency}* payment choice for that combination — the +subscription will then not be offered for sale in +that currency. + +If the merchant backend has no token families configured, the +form shows a warning and offers nothing to edit. In that case +you can still operate Turnstile in pure pay-per-article mode; +simply skip this configuration step. + +Whenever this form is saved, Turnstile republishes **every** +price-category template against the backend so the new +subscription prices appear in the wallet's *Buy subscription* +choices immediately. + +.. note:: + + GNU Taler token families offer more flexible payment options, not just + subscriptions. However, only subscriptions are supported at this + time. + + +.. _drupal-paivana-price-categories: + +Defining price categories +------------------------- + +A *price category* is a named bucket of payment options that can +be attached to nodes. Categories are managed at +``/admin/structure/taler-turnstile-price-categories`` (also +reachable as *Structure → GNU Taler Turnstile price +categories*). The collection page lists the existing categories +together with their machine names and descriptions and offers +*Edit*, *Delete* and *Add price category* actions: + +.. image:: screenshots/drupal-turnstile-price-category-list.png + + +Creating or editing a price category +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Click *Add price category* (or *Edit* on an existing row) to +reach the price-category form. The form has three sections: + +- **Name** — human-readable label for the category (e.g. + *Standard article*, *Long-form feature*). Shown to editors + when they pick a category on a node form. +- **Machine name** — the slug used to build the merchant + template ID (``turnstile-{machine_name}``). Derived + automatically from the initial **Name**; fixed once the + category exists. +- **Description** — free text shown to editors in the node form + to help them pick the right category. +- **Prices** — a fieldset with one collapsible *Details* block + per known token family plus a special block labelled + ``No reduction`` that holds the price for non-subscribers. Each + block contains one numeric input per supported currency. + +.. image:: screenshots/drupal-turnstile-price-category-edit.png + +Pricing semantics: + +- A value in the ``No reduction`` block sets the **per-article price + for visitors without a subscription** in that currency. Leave + it empty to make the article available **only** to + subscribers in that currency. +- A value in a subscription block sets the **discounted price + for holders of that subscription token** in that currency. + A value of exactly ``0`` marks the article as *fully covered* + by that subscription — subscribers see it for free. An empty + field means "this subscription does not unlock articles of + this category in this currency", forcing subscribers + (with this type of subscription) to pay the full per-article + price. + +At least one price (across all blocks and currencies) must be +set; otherwise the form refuses to save as there would be no +way to purchase articles in that price category otherwise. + +Saving the form publishes the category as a +``paivana``-style template to the merchant backend (the v1 +``choices`` array is built from the price grid; see +:ts:type:`TemplateContractPaivana` in the +:ref:`Merchant Backend HTTP API <merchant-api>`). If the local +save succeeds but the backend push fails (network error, 401, +...), the form surfaces an error message. + +.. note:: + + In this case, the local price in Drupal and the merchant template + may have drifted out of sync. Visitors would still pay the price in + the merchant backend, so this error should always be investigated. + +Editing a category, renaming it, or changing prices +automatically refreshes the matching merchant template. +Deleting a category removes its template; if the delete on the +backend fails, you are warned to clean it up manually. + + +Gating individual nodes +----------------------- + +Once a content type is *enabled* in the basic settings (see +above) and at least one price category exists, every node of +that content type gains a new *Price category* field in its +editing form. By default the field is placed in the *meta* +group on the right-hand side of the node form. + +.. image:: screenshots/drupal-turnstile-node-edit.png + +To paywall a node: + +1. Edit or create a node of an enabled content type. +2. Pick a price category in the *Price category* field. +3. Save the node. + +Leaving the field empty leaves the node freely accessible — the +paywall logic only fires for nodes that reference a category. + +If you want to remove the paywall from a node, edit it and clear +the field. If you want to lift the paywall from an entire +content type, untick it in the *Enabled content types* list on +the settings form; the field and its values are then removed. +(The field's contents are dropped, so re-enabling the content +type later starts from a clean slate, forcing you to set the +price categories again for all nodes in that category.) + + +The visitor experience +---------------------- + +Anonymous visitor without a cookie +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For a node that references a price category, an anonymous +visitor without a valid ``taler_turnstile_paivana`` cookie sees: + +- The node rendered in its *teaser* view mode. A soft fade-out + is applied at the bottom of the teaser so it is visually clear + that there is more content behind the paywall. +- A *Payment required* block immediately below the teaser, with + a QR code (encoding the ``taler://pay-template/…`` URI for the + matching merchant template), a *Scan with your GNU Taler + wallet* hint, and — when the browser advertises support for + the ``taler`` URI scheme — an *Open GNU Taler payment Web + page* button. +- A pricing summary showing the per-article price (or "Not sold + individually" if only subscriptions unlock the category) and + the list of subscriptions that accept this category. +- A *Waiting for payment…* status line that updates once the + wallet picks up the order. + +.. image:: screenshots/drupal-turnstile-paywall-page.png + +The page is identical for every visitor without the cookie, so +Drupal's page cache serves it without per-request work. The +``taler://pay-template/…`` URI is also advertised as a +``Paivana:`` HTTP header for non-JavaScript Taler clients. + +Behind the scenes, the JavaScript on the page does **not** +create an order in the merchant backend. It generates a fresh +nonce, derives a session ID from it, renders the QR code, and +long-polls the public ``sessions/{paivana_id}`` endpoint on the +merchant backend waiting for the wallet to pick the template up +and pay. No order, no PHP session and no database write happens +until the visitor's wallet actually engages with the template. + + +Paying +^^^^^^ + +The visitor scans the QR code with a `GNU Taler wallet +<https://docs.taler.net/wallet/>`__ (or clicks the *Open GNU +Taler payment Web page* button when running a browser with a +wallet extension). The wallet shows the available payment +choices — for a typical category these are: + +- *Pay in {currency}* — the per-article price for non-subscribers. +- *Pay in {currency} with subscription* — only offered to visitors + whose wallet holds a matching, unspent subscription token (in + which case it usually costs ``{currency}:0``). +- *Buy subscription in {currency}* — pays the per-article price + plus the subscription's purchase price (from + :ref:`drupal-paivana-subscription-prices` above) and yields a + fresh subscription token in the visitor's wallet. + +The visitor picks an option, the wallet pays, and Drupal's +JavaScript proceeds to the confirmation step. + + +Confirmation and unlock +^^^^^^^^^^^^^^^^^^^^^^^ + +Once the merchant backend reports the order as paid, the +JavaScript POSTs ``{order_id, nonce, cur_time, website}`` to +``/taler-turnstile/paivana``. The Drupal-side controller +re-derives the session ID, fetches the order from the merchant +backend and **verifies that the paid contract's +``fulfillment_url`` matches the requesting page and that the +paid amount is one of the amounts the price category accepts**. +Only then does it mint the ``taler_turnstile_paivana`` cookie +and 303-redirect the browser back to the article. + +The cookie is keyed on Drupal's per-site ``private_key``, bound +to the visitor's client IP and to the specific fulfillment URL, +marked ``HttpOnly``, ``SameSite=Lax``, and ``Secure`` when the +page was served over HTTPS. It is valid for 24 hours (the +``ORDER_VALIDITY_SECONDS`` constant in the API service; this +will follow the merchant-supplied +``pay_deadline``/``max_pickup_time`` once backend v1.6 lands). +The cookie covers exactly one fulfillment URL — clearing +cookies forgets paid access, and access to a different +paywalled article requires a fresh payment (or a valid +subscription). + +.. note:: + + ORDER_VALIDITY_SECONDS should probably be made configurable + in the future, as should the restriction of the client to + a particular IP address, as that may or may not be desired. + See #11444. + + +Subscriptions in action +^^^^^^^^^^^^^^^^^^^^^^^ + +When a paid contract bought a subscription token, Turnstile +also stores the token family slug and the token's expiration +time in the visitor's PHP session. Subsequent requests to +other articles whose price category is fully covered by the +same subscription short-circuit the paywall entirely: the +visitor never sees the *Payment required* block again until the +token expires. + +Visitors without a session cookie still see the paywall page +(so anonymous, cache-friendly traffic stays cacheable). + + +Operational notes +----------------- + +Logging +^^^^^^^ + +The module logs to the dedicated ``taler-turnstile`` channel. +To watch the live event stream while debugging: + +.. code-block:: shell-session + + $ drush watchdog:tail --type=taler-turnstile + +Levels are used meaningfully: ``debug`` for flow tracing, +``info`` for expected skips (for example, a subscription slug +the backend no longer advertises), ``warning`` for user-fixable +configuration issues, and ``error`` for protocol violations or +unexpected HTTP statuses from the backend. + + +Cache management +^^^^^^^^^^^^^^^^ + +The module declares ``cookies:taler_turnstile_paivana`` as a +cache context on every paywalled response, so Drupal's internal +page cache and ``dynamic_page_cache`` separate visitors with a +cookie from those without. A reverse-proxy cache in front of +Drupal must therefore honour the ``Vary: Cookie`` header to +avoid handing one paying visitor's full-content response to +another visitor. + + +Configuration import/export +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Both ``taler_turnstile.settings`` and each +``taler_turnstile_price_category.*`` config entity participate in +the normal Drupal ``drush cex`` / ``drush cim`` workflow. +Importing a configuration change that adds or removes content +types from ``enabled_content_types`` triggers field +add/removal; importing a price-category change republishes the +matching merchant template. + + +Permissions +^^^^^^^^^^^ + +Two permissions are exposed: + +- **Administer GNU Taler Turnstile** — grants access to the + *basics* and *subscription prices* settings forms. +- **Administer price categories** — grants access to the + price-category CRUD pages. + +Both default to *restrict access* and should normally only be +granted to trusted administrators. diff --git a/paivana-httpd-manual.rst b/frags/paivana-httpd-manual.rst diff --git a/wordpress-paivana-manual.rst b/frags/wordpress-paivana-manual.rst diff --git a/screenshots/drupal-turnstile-module-enable.png b/screenshots/drupal-turnstile-module-enable.png Binary files differ. diff --git a/screenshots/drupal-turnstile-node-edit.png b/screenshots/drupal-turnstile-node-edit.png Binary files differ. diff --git a/screenshots/drupal-turnstile-paywall-page.png b/screenshots/drupal-turnstile-paywall-page.png Binary files differ. diff --git a/screenshots/drupal-turnstile-price-category-edit.png b/screenshots/drupal-turnstile-price-category-edit.png Binary files differ. diff --git a/screenshots/drupal-turnstile-price-category-list.png b/screenshots/drupal-turnstile-price-category-list.png Binary files differ. diff --git a/screenshots/drupal-turnstile-settings-form.png b/screenshots/drupal-turnstile-settings-form.png Binary files differ. diff --git a/screenshots/drupal-turnstile-subscription-prices.png b/screenshots/drupal-turnstile-subscription-prices.png Binary files differ. diff --git a/screenshots/taler-turnstile-price-categories.png b/screenshots/taler-turnstile-price-categories.png Binary files differ. diff --git a/taler-paivana-manual.rst b/taler-paivana-manual.rst @@ -23,9 +23,6 @@ Paivana Operator Manual Introduction ============ -About Paivana -------------- - Paivana is an approach to implement paywalls using GNU Taler. For the seller, the Paivana design offers a scalable, cachable, static paywall @@ -79,8 +76,8 @@ and configured to process orders. When setting up the respective Paivana service, you must have the base URL, username and password of the selected merchant backend at hand. -.. include:: paivana-httpd-manual.rst +.. include:: frags/paivana-httpd-manual.rst -.. include:: drupal-paivana-manual.rst +.. include:: frags/drupal-paivana-manual.rst -.. include:: wordpress-paivana-manual.rst +.. include:: frags/wordpress-paivana-manual.rst