# Changelog

All notable changes to Expandable Paywall are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

**This file is the source of truth.** `README.txt` also carries a `== Changelog ==`
section because wordpress.org renders it on the plugin page, but that section is a
condensed copy produced at release time — write entries here first.

**Security entries stay deliberately brief.** This file ships inside the plugin package,
so it is as public as the wordpress.org plugin page. An entry naming the endpoint,
parameter, or precondition involved is a working recipe against every site that has not
updated yet, and plugin users are enumerable. Say what changed and what an administrator
has to do about it; keep the mechanism in the commit message, where the private
repository is the only audience. A fuller write-up can follow once installs have had time
to update.

History before 2.1.11 was reconstructed from `README.txt`, with release dates taken
from the wordpress.org SVN tag creation dates at
<https://plugins.svn.wordpress.org/expandable-paywall/tags/>. Entries keep their
original wording wherever possible so they can be checked against that source.
Version comparison links start from 2.1.11, the first release tagged in git.

## [Unreleased]

## [2.2.2] - 2026-08-07

### Fixed

- Lock icons appear again on protected posts that are listed on a page or on a
  single article, so "Show locks" works with TagDiv Newspaper flex blocks and
  Cloud Library templates, on static front pages, and in related-post,
  previous/next and recent-post listings. The article being read still has no
  lock on its own heading. Regression introduced in 2.1.11.

## [2.2.1] - 2026-08-03

### Changed

- The category, tag and taxonomy fields on the paywall settings screen search as you type
  instead of listing every term in the page. Sites that group content finely have tens of
  thousands of tags, and since 2.2.0 offers a field per taxonomy, all of them were being
  written into the screen at once: on a site with 20,000 tags that was close to a megabyte
  of markup for that one field, and the browser then had to build every one of those
  entries before the screen could be used. Only the terms already selected are in the page
  now, with the rest fetched fifty at a time as you type. What is protected, and how, is
  unchanged.

### Fixed

- The paywall settings screen showed nothing selected under Categories, Tags and any other
  taxonomy on sites upgrading from 2.1, even though the categories chosen there were still
  protecting posts on the front end. Saving the screen in that state then cleared the
  selection for real, leaving the site with no protected categories at all. **If you saved
  the paywall settings on 2.2.0 and paywalled posts have since been readable by everyone,
  re-select your categories on that screen and save.**
- Saving a settings form that does not contain the taxonomy fields, which is any paywall
  type other than Basic, no longer clears the protected categories and terms.

## [2.2.0] - 2026-07-31

### Security

- The "Enable Structured Data for Search Engines" option no longer places the full article
  in the page. It previously wrote the entire post body into a hidden `div`, so anyone
  could read a paywalled article by opening View Source, and it did so on every post
  regardless of whether that post was protected. The option now adds only a short
  machine-readable description telling search engines the article is paywalled; the article
  itself stays on the server. **Any site with this option enabled has been serving its
  paywalled articles to everyone, and should update.**

### Added

- Protect posts by tag or by any custom taxonomy, alongside categories. Each taxonomy that
  applies to your paywalled post types gets its own field on the settings screen, and a
  post is protected if it matches any one of them, so a category called Protected and a tag
  called Insider can both be in force at once. Hierarchical taxonomies continue to match
  terms beneath the ones you select. Existing category selections carry over untouched.
- Create subscriber roles from the settings screen. A new role is a clone of the default
  WordPress Subscriber role under a name of your choosing, carrying the same capabilities,
  which is read access only. Roles are never deleted by the plugin, including at uninstall,
  because removing a role takes access away from everyone holding it.
- Optional full-text indexing for paywalled articles. With "Let verified search engine
  crawlers read the whole article" enabled, requests from the addresses Google and Bing
  publish for their crawlers receive the complete article, so it can be indexed and ranked
  on all of its text, while every other visitor still gets the teaser. **Off by default,
  including on upgrade.** A user agent claiming to be Googlebot is not enough; the source
  address has to match a published range, and the ranges are fetched on a schedule rather
  than during a page request.
- The settings screen reports whether the crawler address lists have been fetched and
  whether a detected page cache needs configuring by hand, since either can stop that
  feature working with no visible sign.
- Filters: `expandable-paywall_protected_terms` and
  `expandable-paywall_protectable_taxonomies` for taxonomy protection,
  `expandable-paywall_structured_data` for adjusting or suppressing the markup when an SEO
  plugin already describes the article, and `expandable-paywall_client_ip` for sites behind
  a proxy known to overwrite the forwarded address header.

### Changed

- Installing the plugin no longer creates a "Paid Subscriber" role by itself. Roles are
  created only when you ask for one, under Registration &amp; Authentication Roles.
  **Existing sites are unaffected**: roles live in the database, so any role already
  created stays exactly as it is, along with every user assigned to it.
- Paywall structured data is emitted as JSON-LD in the page head, added only to posts that
  are actually protected, and declares `isAccessibleForFree` correctly. It previously
  marked every post as paywalled, including free ones.

### Fixed

- Sixteen strings could never be translated. Eight declared a text domain belonging to
  other plugins, and every visible string in the login and registration forms was passed to
  a translation function as a variable, which translation tooling cannot read. The bundled
  translation template was also empty and six years old. It now carries all 97 strings.
- The upgrade notice on the Plugins screen showed its own markup as literal text.
- Deprecated jQuery shorthands replaced, and server error messages are inserted as text
  rather than markup.

### Removed

- Plugin data is now cleaned up on uninstall: options, bypass codes and the log table.
  Roles are deliberately left in place, because deleting one takes access away from every
  user who holds it.

## [2.1.11] - 2026-07-30

### Security

- Hardened account creation through the paywall registration form. It is now off by
  default, behind a new "Let visitors create an account through the paywall registration
  form" setting. **Sites that use the registration form must enable this setting after
  upgrading.**
- Hardened the roles available as subscriber roles. Roles that can edit or manage the
  site are no longer offered in the settings screen, accepted if saved, or assigned to a
  subscriber. A site with such a role saved will see a notice on the settings screen and
  have Subscriber used until a different role is chosen.
- Registration now validates the submitted email address and requires a password.
- Bypass links created from now on use a cryptographically random code. Links already
  issued for existing posts are left alone and keep working, so nothing shared with
  readers breaks.
- Reading and clearing the debug log now requires the same permission as the screen that
  displays them, and neither is reachable by a logged-out visitor.
- The Log screen no longer opens its own database connection using the credentials from
  `wp-config.php`, and no longer prints database error text to the browser. It uses the
  same connection as the rest of WordPress, so it works on hosts where the database is
  reached over a port or a socket, and on installs using a database drop-in.
- Category, post type and role names are escaped where the paywall settings screen prints
  them, so a name holding markup can no longer affect that page.
- The "Forgot your password?" link in the login form escapes its URL, so a value that is
  not a real web address cannot affect the page. When no URL is configured the link is
  omitted rather than rendered pointing nowhere.

### Changed

- DataTables is now bundled with the plugin instead of being loaded from
  `cdn.datatables.net` on the Log screen. Nothing about the screen changes, but the plugin
  no longer sends an administrator's IP address to a third party, and the Log screen keeps
  working without outbound internet access. Only the parts actually used are included, so
  the download is about 117 KB smaller.

### Added

- Setting to allow visitors to register through the paywall registration form, under
  Registration &amp; Authentication Roles. Off by default, and independent of the WordPress
  "Anyone can register" setting so enabling one does not open the other.
- `expandable-paywall_registration_is_open` filter, for overriding that setting or
  attaching a CAPTCHA or rate limit.
- `expandable-paywall_subscriber_roles` filter, for adjusting the roles offered as
  subscriber roles.
- `expandable-paywall_registration_blocked` action, fired when a registration attempt is
  refused because registration is closed.

### Fixed

- Login failing for usernames and email addresses containing an apostrophe or quote.
  WordPress adds slashes to `$_GET`, `$_POST` and `$_SERVER`, and the submitted value was
  never unslashed, so `o'brien@example.com` was looked up as `o\'brien@example.com` and
  never matched. All request and server values are now unslashed before sanitizing.
- Login failing for any password containing `<` or `>`. The submitted password was run
  through `sanitize_text_field()`, which strips tags, so it no longer matched the stored
  hash. Affected users could still sign in at `wp-login.php`, but not through the
  plugin's own login form or shortcode.
- Logged user agents no longer store escaped slashes, and a missing `User-Agent` header
  no longer raises a PHP notice on WP-Cron, WP-CLI and proxied requests.
- Fatal error on the front end when the paywall was enabled but its settings had never
  been saved, which took out the site with a white screen on PHP 8.
- Filters registered on `init`, which is where add-ons and themes normally register them,
  were ignored for the protected categories and the roles that bypass the paywall. Both
  were read once while the plugin was still loading and reused for the rest of the
  request, so anything hooked afterwards had no effect. They are now read where they are
  used.
- A role added to `expandable-paywall_roles_that_bypass_paywall` by an add-on was
  discarded rather than kept, because the plugin's own callback on that filter returned a
  cached list instead of the value passed to it.
- A second `[expandable-paywall-login]` or `[expandable-paywall-register]` shortcode on the
  same page rendered nothing. Both forms now render every time they are used.
- Paywall bypass links were never generated for posts saved in the block editor, leaving
  the Paywall Bypass Link box permanently showing "You need to save this post before a
  bypass link will be generated". Code assignment ran only when an editor screen was
  loaded rather than when a post was saved, and block editor saves go through the REST
  API. Posts now get a bypass link however they are saved, including via WP-CLI and the
  REST API.

## [2.1.10] - 2025-07-03

### Changed

- Login form redirect now preserves the path and appends a cache-busting parameter.

## [2.1.9] - 2025-07-02

### Changed

- Login form shortcode now redirects back to the page you were on when logging in.

## [2.1.8] - 2025-06-20

### Fixed

- Removed leftover debugging code.

## [2.1.7] - 2025-06-19

### Fixed

- `load_plugin_textdomain` PHP notice.

## [2.1.6] - 2025-06-09

### Added

- Filter for controlling when to show `show_authentication_role_paid`.

### Changed

- Updated all third-party vendor packages.

## [2.1.5] - 2025-02-09

### Changed

- Updated all third-party vendor packages.

## [2.1.4] - 2024-08-02

### Changed

- Excluded the search screen from triggering the paywall.

## [2.1.3] - 2024-06-20

### Added

- Restored a stub function in the public class for extendability.

## [2.1.2] - 2024-06-19

### Fixed

- 500 error when creating a new user on specific installs.

## [2.1.1] - 2024-06-20

### Added

- New helper functions.

### Fixed

- Corrected logic for displaying the paywall.

## [2.1.0] - 2024-06-20

### Added

- Many more functions on the core `Helper` class.

### Changed

- Updated all third-party libraries to their latest versions.
- Refactored all code to PHP 8.1 standards.

### Fixed

- PHP warning in the `login.php` template.

## [2.0.23] - 2024-01-21

### Fixed

- Protecting categories and subcategories on some installs.

## [2.0.22] - 2024-01-12

### Added

- Helper function to check whether the paid authentication user role should show.
- Function to clear logs.

## [2.0.21] - 2024-01-12

### Added

- Option to disable the paywall.

### Changed

- Any subcategory under a protected category is now automatically protected.

## [2.0.20] - 2023-11-22

### Added

- New logging section.

## [2.0.19] - 2023-11-09

### Changed

- Updated add-on handling and vendor paths.

## [2.0.18] - 2023-11-06

### Changed

- Updated all third-party vendor libraries.

### Fixed

- Protected content could not be seen by some users.

## [2.0.17] - 2023-07-05

### Changed

- Updated all third-party vendor libraries.

## [2.0.16] - 2022-10-27

### Added

- Post meta box that generates a unique bypass link so an article can skip the paywall.

## [2.0.15] - 2022-10-14

### Added

- Filter allowing the paywall to be bypassed programmatically.

## [2.0.14] - 2022-09-15

### Added

- After logging in, the user is redirected back to where they were with
  `?cache_busting=RANDOM`.

### Changed

- Updated the logic for when locks appear on articles.

## [2.0.13] - 2022-09-15

### Fixed

- Archives were not excluded from the protection level.

## [2.0.12] - 2022-08-31

### Changed

- Updated to the latest version of Freemius.
- Added more error reporting options to `error_log`.

### Fixed

- Roles were not being read in certain instances.
- Locks were not showing when enabled.

## [2.0.11] - 2022-03-18

### Changed

- When debug is turned on, an email is sent to the site admin.

## [2.0.10] - 2022-03-01

### Changed

- Updated vendor libraries.

## [2.0.9] - 2022-01-25

### Added

- `[expandable-paywall-protect]` shortcode for protecting sections of content on any
  page or post.

## [2.0.8] - 2021-07-13

### Changed

- Updated all vendor libraries.
- Code cleanup and refactoring.

## [2.0.7] - 2021-05-03

### Added

- Debug mode.
- Filter allowing the basic role to be set.

## [2.0.6] - 2021-04-27

### Fixed

- Authentication now accounts for case sensitivity.

### Removed

- Erroneous `error_log` calls.

## [2.0.5] - 2021-04-21

### Fixed

- Lock icon appearing on non-protected pages.

## [2.0.4] - 2021-04-02

### Added

- Lock icon next to protected posts.
- Fade-out of the excerpt into the paywall, with default styles.
- Schema structured data output.
- Logging function in the helper files.

### Changed

- Updated to the latest version of the Freemius SDK.
- More support for multisite installs.

## [2.0.3]

Released without an SVN tag, so no release date is recorded.

### Added

- Logout shortcode.

### Changed

- Updated paywall CSS styles.

## [2.0.1] - 2021-02-25

### Fixed

- Improper function calls when initially installing.

## [2.0.0] - 2021-02-22

### Changed

- Reworked a large number of filters and hooks.
- Updated all third-party SDKs.

## [1.1.3] - 2020-08-06

No changelog entry was recorded for this release.

## [1.1.2] - 2020-03-04

### Fixed

- Registration threw a 500 error behind the scenes.

## [1.1.1] - 2020-03-04

### Fixed

- Data sanitization issues.

## [1.1.0]

Initial version. Released before the plugin was tagged on wordpress.org, so no
release date is recorded.
