wordpress-paivana-manual.rst (23924B)
1 .. 2 This file is part of GNU TALER. 3 4 Copyright (C) 2026 Taler Systems SA 5 6 TALER is free software; you can redistribute it and/or modify it under the 7 terms of the GNU Affero General Public License as published by the Free Software 8 Foundation; either version 2.1, or (at your option) any later version. 9 10 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 11 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 13 14 You should have received a copy of the GNU Affero General Public License along with 15 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 16 17 @author Christian Grothoff 18 19 .. _wordpress-paivana: 20 21 22 Wordpress integration 23 ===================== 24 25 .. note:: 26 27 This chapter is still early work in progress. 28 29 This chapter documents the installation and operation of the 30 ``taler-turnstile`` Wordpress plugin. The plugin gates individual Wordpress 31 posts (and any other selected public post type) behind a GNU Taler paywall: 32 visitors who have not yet paid are shown a teaser plus a payment widget, and 33 once their wallet has paid the associated merchant template the full post 34 content is unlocked. 35 36 Like its :ref:`Drupal counterpart <drupal-paivana>`, the plugin is built 37 around the same Paivana-style flow as :ref:`paivana-httpd <Paivana-httpd>`: a 38 static, cacheable paywall page references a *payment template* in the merchant 39 backend, no order is created when a visitor merely loads a paywalled article, 40 and no PHP session is started for unauthenticated traffic. This keeps bot 41 traffic from polluting the merchant backend and lets Wordpress' page cache (or 42 any reverse-proxy cache in front of it) serve the paywall page to anonymous 43 visitors without per-request work. 44 45 46 Architecture overview 47 --------------------- 48 49 A Wordpress site using Turnstile combines three independent 50 components: 51 52 1. **The Wordpress site.** An ordinary Wordpress 6.3+ installation 53 serving one or more public post types (``post``, ``page``, custom 54 post types, ...). The ``taler-turnstile`` plugin adds a 55 *Price Category* meta field (stored as the post meta key 56 ``_taler_price_category``) to the post types you select, and 57 hooks ``the_content`` and ``template_redirect`` to intercept 58 rendering of posts that carry a non-empty value. 59 2. **A GNU Taler merchant backend** (``taler-merchant-httpd``). 60 The backend stores one *payment template* per price category 61 defined in Wordpress, manages orders, talks to one or more Taler 62 exchanges, and ultimately reports back whether a given order 63 has been paid. See the 64 :ref:`Taler Merchant Backend Operator Manual 65 <taler-merchant-backend-operator-manual>` for full details. 66 3. **The visitor's GNU Taler wallet.** The wallet scans the QR 67 code shown on the paywall page (or follows the 68 ``taler://pay-template/…`` link), pays the merchant template 69 and triggers the unlock. 70 71 Turnstile itself stores no per-visitor state in the Wordpress database; once 72 the merchant backend confirms a payment, Turnstile mints a short-lived HMAC 73 cookie (``taler_turnstile_paivana``, keyed by Wordpress' built-in 74 ``wp_salt('auth')``) that the browser presents on subsequent requests to the 75 same fulfillment URL. Clearing cookies forgets paid access, but GNU Taler 76 wallet's repurchase detection will restore the cookie if the user attempts to 77 pay for the same article again. 78 79 .. note:: 80 81 This feature likely does not interact well with expiration 82 periods for article purchases. This is a bug that will 83 require further work to fully address. See #11443. 84 85 86 Installation 87 ------------ 88 89 Requirements 90 ^^^^^^^^^^^^ 91 92 - Wordpress 6.3 or later 93 - PHP 8.0 or later 94 - **Pretty permalinks must be enabled.** With the default plain 95 permalinks (``?p=123``) every post is served from path ``/``, 96 which means the path-scoped access cookie cannot be isolated 97 per article; the plugin therefore refuses to paywall posts in 98 that configuration and shows an admin notice on the settings 99 pages until pretty permalinks are turned on under 100 *Settings → Permalinks*. 101 - A reachable GNU Taler merchant backend supporting the v29 (or 102 newer) ``paivana`` template type and the public 103 ``/sessions/$SESSION_ID`` endpoint 104 - An access token for the merchant instance with the 105 ``templates-write`` and ``orders-read`` permissions 106 107 108 Obtaining the plugin 109 ^^^^^^^^^^^^^^^^^^^^ 110 111 The plugin sources are available from the GNU Taler Git repository as well as 112 Wordpress' own plugin directory. Download the latest release and extract it 113 into your Wordpress installation's ``wp-content/plugins/`` directory so that 114 the resulting layout is: 115 116 .. code-block:: shell-session 117 118 $ ls wp-content/plugins/taler-turnstile/ 119 admin/ assets/ includes/ 120 COPYING README.md taler-turnstile.php 121 122 There is no build step and no Composer package: the plugin is plain PHP and a 123 handful of JavaScript files. The QR code library ``assets/js/qrcode.min.js`` 124 is vendored from `davidshimjs/qrcodejs 125 <https://github.com/davidshimjs/qrcodejs>`__ and must be present for the QR 126 code to render; see ``assets/js/QRCODE-README.md`` in the plugin directory for 127 the download instructions if you are building from source. 128 129 130 Activating the plugin 131 ^^^^^^^^^^^^^^^^^^^^^ 132 133 Once the directory is in place, activate the plugin from 134 *Plugins → Installed Plugins* and press *Activate* on the 135 *GNU Taler Turnstile* row: 136 137 .. image:: screenshots/wordpress-turnstile-plugin-activate.png 138 139 Activation registers the plugin's options with their defaults 140 (by default, the ``post`` post type is enabled and the backend 141 URL/token are left blank) and adds three new admin menu entries: 142 143 - *Settings → Taler Turnstile* (main settings), 144 - *Settings → Taler Subscriptions* (subscription purchase prices), 145 - *Taler Prices* (top-level menu, price-category management). 146 147 Deactivating the plugin removes the meta box and stops the 148 paywall from rendering, but it does currently **not** delete templates 149 already published to the merchant backend; remove those manually 150 if desired (see :ref:`template`). 151 152 153 Configuring the basics 154 ---------------------- 155 156 The main settings page lives at 157 ``wp-admin/options-general.php?page=taler-turnstile-settings`` 158 and is also reachable through *Settings → Taler Turnstile*. It 159 collects the four pieces of information Turnstile needs to talk 160 to the merchant backend and to know which types of content to gate: 161 162 .. image:: screenshots/wordpress-turnstile-settings-form.png 163 164 The fields are: 165 166 - **Enabled Post Types** — a list of checkboxes derived from 167 every *public* post type registered on the site (the built-in 168 ``post`` and ``page``, plus any custom post types). Each 169 ticked post type gains the *Taler Turnstile Price Category* 170 sidebar meta box on its editor screen, and gets it removed 171 again when unticked. Disabling every post type triggers a 172 one-shot cleanup that drops the ``_taler_price_category`` meta 173 from every post on the site, so re-enabling a post type later 174 starts from a clean slate. 175 - **Payment Backend URL** — the HTTP(S) base URL of the merchant 176 backend. Instance-specific URLs ending in ``/instances/$ID/`` 177 are accepted; the URL **must end with a trailing slash**. When 178 you save the form with a non-empty value, Turnstile contacts 179 the backend's ``/config`` endpoint to verify reachability and 180 refuses to save the value if the backend does not respond as a 181 ``taler-merchant`` instance. 182 - **Access Token** — the Bearer token used for every call to the 183 backend. It **must begin with** ``secret-token:`` (following 184 RFC 8959, see :ref:`Access control 185 <taler-merchant-backend-operator-manual>` in the merchant 186 manual). When both URL and token are present, the form 187 performs a live authentication check against the backend and 188 surfaces the HTTP status as an admin notice if the access 189 token is wrong (401/403), the instance does not exist (404), 190 or the backend is otherwise unreachable. 191 - **Disable Turnstile on Error** (fail-open toggle, off by 192 default) — when the merchant backend cannot be reached, this 193 controls whether visitors see the unpaywalled article 194 (checked) or an error message in place of the payment widget 195 (unchecked). The settings form warns you with an admin 196 notice if you leave the toggle off while the backend URL or 197 access token is missing, so a misconfiguration cannot 198 silently break the live site. 199 200 The first time you save a working backend URL and access token, 201 Turnstile pushes a ``paivana``-style template to the merchant 202 backend for every price category that already exists. Each 203 template carries the ID ``turnstile-{category_id}``. You do not 204 need to create those templates by hand, and you do not need to 205 visit the merchant backend's SPA to manage them — Turnstile keeps 206 them in sync as you edit the categories (see 207 :ref:`wordpress-paivana-price-categories` below). Saving a new 208 backend URL or access token also republishes every existing 209 price-category template, on the assumption that the new instance 210 does not have them yet. 211 212 Before continuing, make sure that the merchant backend instance 213 itself is ready to take payments: 214 215 - The instance has at least one configured 216 :ref:`bank account <instance-bank-account>`. 217 - Any legitimization required by the chosen payment service 218 provider has been completed. 219 220 221 .. _wordpress-paivana-subscription-prices: 222 223 Configuring subscription prices 224 ------------------------------- 225 226 .. note:: 227 228 The term *subscription* is slightly misleading, as it is not 229 auto-renewing. We are in the process of renaming this to *passes* which is 230 clearer. However, the current plugin uses subscription and thus so does this 231 manual. 232 233 Turnstile supports the GNU Taler subscription model, in which a visitor buys a 234 *subscription token* once and the token is then consumed to unlock individual 235 articles for some validity period. The catalogue of available *token 236 families* is configured on the merchant backend (see the merchant manual 237 section on :ref:`templates <template>` and the wider Taler concept of 238 subscription tokens); Turnstile reads the catalogue live from 239 ``private/tokenfamilies`` and asks you to set the *purchase price* per token 240 family per currency. 241 242 Navigate to *Settings → Taler Subscriptions* (URL 243 ``wp-admin/admin.php?page=taler-subscription-prices``). The page refuses to 244 render until the backend is configured and reachable — it needs the live 245 token-family list and a complete currency inventory from ``/config``. When 246 the backend is ready, the page lists one *postbox* block per token family 247 advertised by the backend, with one numeric input per supported currency: 248 249 .. image:: screenshots/wordpress-turnstile-subscription-list.png 250 251 Leaving a field empty disables the *Buy subscription in 252 {currency}* payment choice for that combination — the 253 subscription will then not be offered for sale in that currency 254 on any article. 255 256 If the merchant backend has no token families configured, the page shows a 257 warning and offers nothing to edit. In that case you can still operate 258 Turnstile in pure pay-per-article mode; simply skip this configuration step. 259 260 Whenever this page is saved, Turnstile republishes **every** price-category 261 template against the backend so the new subscription prices appear in the 262 wallet's *Buy subscription* choices immediately. 263 264 .. note:: 265 266 GNU Taler token families offer more flexible payment options, 267 not just subscriptions. However, only subscriptions are 268 supported by Turnstile at this time. 269 270 271 .. _wordpress-paivana-price-categories: 272 273 Defining price categories 274 ------------------------- 275 276 A *price category* is a named set of payment options that can be attached to 277 posts. Categories are managed under the top-level *Taler Prices* menu in the 278 admin sidebar (URL ``wp-admin/admin.php?page=taler-price-categories``). The 279 list page shows the existing categories together with their descriptions and 280 offers *Edit*, *Delete* and *Add New* actions: 281 282 .. image:: screenshots/wordpress-turnstile-price-category-list.png 283 284 285 Creating or editing a price category 286 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 287 288 Click *Add New* (or *Edit* on an existing row) to reach the 289 price-category form. The form has two sections: 290 291 - **Name** — human-readable label for the category (e.g. 292 *Standard article*, *Long-form feature*). Shown to editors 293 when they pick a category on a post-edit screen and used as 294 the merchant template's summary text. 295 - **Description** — free text shown to editors and used as the 296 template description on the merchant backend if non-empty. 297 298 Below the meta fields, the form has one *Prices* postbox per known token 299 family, plus a special block labelled *No reduction* that holds the price for 300 non-subscribers. Each block contains one numeric input per supported 301 currency: 302 303 .. image:: screenshots/wordpress-turnstile-price-category-edit.png 304 305 Pricing semantics: 306 307 - A value in the *No reduction* block sets the **per-article 308 price for visitors without a subscription** in that currency. 309 Leave it empty to make the article available **only** to 310 subscribers in that currency. 311 - A value in a subscription block sets the **discounted price 312 for holders of that subscription token** in that currency. A 313 value of exactly ``0`` marks the article as *fully covered* by 314 that subscription — subscribers see it for free. An empty 315 field means "this subscription does not unlock articles of 316 this category in this currency", so a holder of such a 317 subscription has to pay the full per-article price. 318 319 At least one price (across all blocks and currencies) must be set; otherwise 320 the category yields no valid payment choices and the matching template cannot 321 be published. 322 323 Saving the form publishes the category as a ``paivana``-style template to the 324 merchant backend (the v1 ``choices`` array is built from the price grid; see 325 :ts:type:`TemplateContractPaivana` in the :ref:`Merchant Backend HTTP API 326 <merchant-api>`). If the local save succeeds but the backend push fails 327 (network error, 401, ...), the admin notice on the next page surfaces the 328 error. 329 330 .. note:: 331 332 In this case, the local price in Wordpress and the merchant 333 template may have drifted out of sync. Visitors would still 334 pay the price in the merchant backend, so this error should 335 always be investigated. 336 337 Editing a category or changing prices automatically refreshes the matching 338 merchant template via ``PATCH``. Deleting a category removes its template via 339 ``DELETE``; if the delete on the backend fails, you are warned to clean it up 340 manually. 341 342 343 Gating individual posts 344 ----------------------- 345 346 Once a post type is enabled in the basic settings (see above) 347 and at least one price category exists, every post of that 348 post type gains a new *Taler Turnstile Price Category* sidebar 349 meta box on its editor screen (shown in the bottom right corner): 350 351 .. image:: screenshots/wordpress-turnstile-post-meta-box.png 352 353 To paywall a post: 354 355 1. Edit or create a post of an enabled post type. 356 2. Pick a category in the *Price Category* drop-down on the 357 sidebar meta box. 358 3. *Update* / *Publish* the post. 359 360 Leaving the drop-down on *-- None --* leaves the post freely accessible — the 361 paywall logic only fires for posts that reference a category. 362 363 If you want to remove the paywall from a post, edit it and change the meta box 364 back to *-- None --*. If you want to lift the paywall from an entire post 365 type, untick it in the *Enabled Post Types* list on the settings page; the 366 meta box and the post meta are then removed from every post of that post type. 367 Re-enabling the post type later starts from a clean slate, forcing you to set 368 the price categories again for all posts of that type. 369 370 371 The visitor experience 372 ---------------------- 373 374 Anonymous visitor without a cookie 375 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 376 377 For a post that references a price category, an anonymous 378 visitor without a valid ``taler_turnstile_paivana`` cookie sees: 379 380 - The post rendered as its *excerpt* (the explicit ``excerpt`` 381 field, or the first 55 words of the body if none is set). 382 - A *Payment Required* block immediately below the excerpt with 383 a QR code (encoding the ``taler://pay-template/…`` URI for the 384 matching merchant template), a *Scan with your GNU Taler 385 wallet* hint, and — when the browser advertises support for 386 the ``taler`` URI scheme — an *Open GNU Taler payment page* 387 button. 388 - A pricing summary showing the per-article price (or "Not sold 389 individually" if only subscriptions unlock the category) and 390 the list of subscriptions that accept this category. 391 - A *Waiting for payment…* status line that updates once the 392 wallet picks up the order. 393 394 .. image:: screenshots/wordpress-turnstile-paywall-page.png 395 396 The page is identical for every visitor without the cookie, so Wordpress' page 397 cache (and any reverse-proxy cache in front of it) can serve it without 398 per-request work. The response sets ``Vary: Cookie`` so a returning paying 399 visitor's path-scoped access cookie still busts the cached paywall, and the 400 ``taler://pay-template/…`` URI is also advertised as a ``Paivana:`` HTTP 401 header for non-JavaScript Taler clients. 402 403 Behind the scenes, the JavaScript on the page does **not** create an order in 404 the merchant backend. It generates a fresh nonce, derives a session ID from 405 it (the ``paivana_id``), renders the QR code, and long-polls the public 406 ``sessions/{paivana_id}`` endpoint on the merchant backend waiting for the 407 wallet to pick the template up and pay. No order, no PHP session and no 408 database write happens until the visitor's wallet actually engages with the 409 template. 410 411 412 Paying 413 ^^^^^^ 414 415 The visitor scans the QR code with a `GNU Taler wallet 416 <https://docs.taler.net/wallet/>`__ (or clicks the *Open GNU Taler payment 417 page* button when running a browser with a wallet extension). The wallet 418 shows the available payment choices — for a typical category these are: 419 420 - *Pay in {currency}* — the per-article price for non-subscribers. 421 - *Pay in {currency} with subscription* — only offered to 422 visitors whose wallet holds a matching, unspent subscription 423 token (in which case it usually costs ``{currency}:0``). 424 - *Buy subscription in {currency}* — pays the per-article price 425 plus the subscription's purchase price (from 426 :ref:`wordpress-paivana-subscription-prices` above) and yields 427 a fresh subscription token in the visitor's wallet. 428 429 The visitor picks an option, the wallet pays, and Wordpress' 430 JavaScript proceeds to the confirmation step. 431 432 433 Confirmation and unlock 434 ^^^^^^^^^^^^^^^^^^^^^^^ 435 436 Once the merchant backend reports the order as paid, the JavaScript POSTs 437 ``{order_id, nonce, cur_time, website}`` to the plugin's REST endpoint at 438 ``/wp-json/taler-turnstile/v1/paivana``. The Wordpress-side controller 439 re-derives the canonical ``paivana_id``, resolves the fulfillment URL back to 440 a local post via ``url_to_postid()``, fetches the order from the merchant 441 backend and **verifies that the paid contract's ``fulfillment_url`` matches 442 the requesting page and that the paid amount is one of the amounts the price 443 category accepts**. Only then does it mint the ``taler_turnstile_paivana`` 444 cookie and instruct the browser to reload the article. 445 446 The cookie is keyed on Wordpress' built-in ``wp_salt('auth')``, bound to the 447 visitor's ``REMOTE_ADDR`` and ``Path``-scoped to the specific fulfillment URL, 448 marked ``HttpOnly``, ``SameSite=Lax``, and ``Secure`` when the page was served 449 over HTTPS. It is valid for 24 hours (the ``ORDER_VALIDITY_SECONDS`` constant 450 in ``Taler_Merchant_API``; this will follow the merchant-supplied 451 ``pay_deadline``/``max_pickup_time`` in the future). The cookie covers 452 exactly one fulfillment URL — clearing cookies forgets paid access, and access 453 to a different paywalled article requires a fresh payment (or a valid 454 subscription). 455 456 The REST confirmation endpoint is unauthenticated by design (the payer has no 457 Wordpress account) and is therefore rate-limited per-IP to at most 30 requests 458 per minute, comfortably above legitimate use but capping the abuse of an 459 endpoint that triggers a merchant-backend round-trip per call. 460 461 .. note:: 462 463 ``ORDER_VALIDITY_SECONDS`` should probably be made 464 configurable in the future, as should the restriction of the 465 client to a particular IP address, as that may or may not be 466 desired. See #11444. 467 468 469 Subscriptions in action 470 ^^^^^^^^^^^^^^^^^^^^^^^ 471 472 When a paid contract bought a subscription token, Turnstile also stores the 473 token family slug and the token's expiration time in the visitor's PHP session 474 (the session is started only *after* the successful payment, so the cacheable 475 pre-payment path is unaffected). Subsequent requests to other posts whose 476 price category is fully covered by the same subscription short-circuit the 477 paywall entirely: the visitor never sees the *Payment Required* block again 478 until the token expires. 479 480 Visitors without a PHP-session cookie still see the paywall page (so 481 anonymous, cache-friendly traffic stays cacheable); the subscriber check is 482 gated on the session cookie already being present so that loading a paywalled 483 post never creates a session on its own. 484 485 486 Operational notes 487 ----------------- 488 489 Caching 490 ^^^^^^^ 491 492 The plugin emits cache-control headers tuned for the two paths it produces: 493 494 - The static paywall response carries ``Vary: Cookie``, so a 495 shared cache (page cache plugin, reverse proxy, CDN) can store 496 one copy for all anonymous visitors while still busting it for 497 returning paying visitors whose path-scoped 498 ``taler_turnstile_paivana`` cookie now accompanies their 499 request. 500 - The post-payment response (the article, served only to the 501 visitor who paid) is marked ``Cache-Control: private, 502 no-store`` and triggers the ``DONOTCACHEPAGE`` define plus 503 ``nocache_headers()`` so it never lands in a shared cache. 504 - The REST confirmation endpoint under ``/wp-json/`` is not 505 page-cached by Wordpress, and its response is explicitly 506 marked ``no-store``. 507 508 A reverse-proxy cache in front of Wordpress must therefore honour the ``Vary: 509 Cookie`` header to avoid handing one paying visitor's full-content response to 510 another visitor. 511 512 513 Logging 514 ^^^^^^^ 515 516 The plugin writes to PHP's ``error_log`` (i.e. the destination your 517 ``php.ini`` or web server configures for ``error_log``). Notable events that 518 are logged include unreachable backends, malformed responses from the merchant 519 backend, paid orders whose contract does not match the fulfillment URL or 520 whose paid amount falls outside the price category, and template 521 create/update/delete failures. When debugging an integration problem, tailing 522 the PHP error log while reproducing the issue is usually the fastest way to 523 identify the culprit. 524 525 526 Permissions 527 ^^^^^^^^^^^ 528 529 All admin pages — the basic settings, the subscription prices and the 530 price-category CRUD — are gated on Wordpress' ``manage_options`` capability, 531 which corresponds to the *Administrator* role. Editors who only need to 532 assign a price category to a post do not need *manage_options*: the sidebar 533 meta box is rendered for any user who can edit the post in question 534 (``edit_post`` capability check on the meta field). 535 536 537 Hooks and filters 538 ^^^^^^^^^^^^^^^^^ 539 540 The plugin exposes a small number of WordPress filters so a theme or 541 site-specific plugin can adapt Turnstile to local conventions without forking 542 it: 543 544 - ``taler_turnstile_enabled_post_types`` — alter the list of 545 post types subject to the paywall transformation. 546 - ``taler_turnstile_payment_choices`` — modify the payment 547 ``choices`` array before it is sent to the merchant backend. 548 - ``taler_turnstile_currencies`` — add to or modify the 549 list of currencies discovered from the backend's ``/config``. 550 551 These hooks are intentionally narrow; deeper customisation (for example, 552 overriding the paywall HTML) requires patching 553 ``Taler_Content_Filter::render_paywall()`` directly.