# Changelog for FetchIt

All notable changes to this project will be documented in this file.

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

## [4.0.0] - 2026-09-25

FetchIt 4 is one package for MODX 2.8 and MODX 3. It replaces the 1.x and 3.x lines and installs over either of them.

### Added

- Types for sites written in TypeScript: `assets/components/fetchit/js/fetchit.d.ts` describes `FetchIt`, the config, `FetchIt.Message`, the instances and the `fetchit:*` events with their `detail`, so `document.addEventListener('fetchit:success', …)` knows `event.detail.response`. The script is type-checked against it, the detail of every event included.
- `FetchIt.createNotifier()`, the built-in notifier for sites that set `FetchIt.Message` themselves; `duration: 0` keeps a toast until it is closed.
- An upgrade with `fetchit.frontend.default.notifier` on says in its log that Notyf is no longer loaded, and warns when `fetchit.frontend.js` is a script of the site.
- Spam protection, on by default, for the AJAX path and forms sent without JavaScript:
  - a signed single-use token in every form (`fetchit_token`, no session needed). Only a request with a well-signed token of the form gets the next one, in the `X-FetchIt-Token` header; the script uses it for the next submission and, when the page's token was stale (a cached page, a page open for long, a new key), sends the form once more by itself;
  - a minimum fill time (`fetchit.protection.min_time`), counted from the first render of the page, retries included;
  - a hidden trap field with a random name per installation that gets a fake success;
  - a limit per form and client address (`fetchit.protection.rate_limit`, `fetchit.protection.rate_window`), also behind trusted proxies (`fetchit.protection.proxies`, `fetchit.protection.ip_header`);
  - used tokens are marked atomically under `core/cache/fetchit/tokens/`, which "Clear cache" leaves alone; when the mark cannot be written, forms are refused and it is logged;
  - `fetchit.protection.log` controls what is logged; the signing key `fetchit.protection.secret` is generated at install.
  - an optional proof of work (`fetchit.protection.pow`, in bits, off by default): the script finds `n` for which SHA-256 of `token:n` starts with that many zero bits and sends it as `fetchit_pow`, starting as soon as the visitor enters the form. When a page from a cache asks for less than the server, the refusal carries `X-FetchIt-Pow` and the script solves and sends once more;
  - optional external captchas (`fetchit.captcha`: `turnstile`, `recaptcha` for reCAPTCHA v3, `smartcaptcha`, with `fetchit.captcha.site_key` and `fetchit.captcha.secret_key`). FetchIt links the script of the provider, gets its answer before each submission and verifies it on the server last of the protection checks, before `OnFetchItBeforeProcess`; reCAPTCHA answers below `fetchit.captcha.min_score` or for another action are refused. When the provider cannot be reached, answers with an error or does not accept the secret key, the form is refused with `fetchit_err_captcha_unavailable` (`X-FetchIt-Refused: captcha_unavailable`) and the cause is logged. When there is no answer to send (the script did not load, the visitor closed the check, the widget failed), the form is not sent and the visitor sees `fetchit_err_captcha_client`. A misspelt provider, a missing key and unusable `fetchit.captcha.min_score` or `fetchit.protection.pow` values are logged.
  `fetchit.protection` turns it all off.
- The `OnFetchItBeforeProcess` event: plugins get `$action`, `$fields`, `$properties` and `$FetchIt`, and refuse a submission with `$modx->event->output('message')`. It also runs with the protection off.
- MODX 3 support in the same package, built on MODX 2.8. It installs fresh on MODX 2.8 and MODX 3, and upgrades FetchIt 1.x on MODX 2 and FetchIt 3.x on MODX 3.
- The FetchIt 3.x API: `$modx->services->get('FetchIt')` on MODX 3 (registered by `bootstrap.php`), and on both versions the `FetchIt\FetchIt` class, `saveActionProperties()` and `getActionProperties()`.
- `FetchIt::service()`, the shared instance on MODX 2 and MODX 3. The 1.x call `$modx->getService('fetchit', …)` and the 3.x `$modx->services->get('FetchIt')` return the same instance.
- `FetchIt::pdoTools()`, which finds pdoTools 2 on MODX 2 and pdoTools 3 on MODX 3 (Fenom and `@FILE` chunks). Without pdoTools, an `@FILE` form is logged as needing it.
- Lexicon entry `fetchit_err_request`, passed to the script as `requestErrorMessage`: the text a visitor sees when a submission fails (see Fixed).
- `detail.error` in `fetchit:error` for failed requests.
- `FetchIt::prepareForm()`, which gives the form tags of a chunk the POST method and the action key.
- Errors in the MODX log when the plugin cannot add the script to a page with a form: no `<head>`, or a `fetchit.frontend.js` that is not a `.js` file.

### Changed

- The built-in notifier (`fetchit.frontend.default.notifier`) is FetchIt's own instead of Notyf, part of `fetchit.js` with no other files. Screen readers hear it through live regions that are on the page from the start (errors at once, successes politely); the close button is labelled from the lexicon (`fetchit_notifier_close`), the focus moves on when a focused toast closes, and a toast stays while hovered or focused. At most three are shown. Its styles use single class selectors, come first in `<head>` and take CSS variables (`--fetchit-toast-{success,error}-{bg,text,border}`), by default pastel green and red of Tailwind CSS 4 as in its alerts: shade 100 for the background, 200 for the border, 800 for the text (in `oklch()`, in hex for older browsers); under a Content-Security-Policy they take the nonce of the FetchIt script. A `FetchIt.Message` of the site with neither `success` nor `error` gets the built-in ones.
- With `fetchit.frontend.default.notifier` on, `window.Notyf` is no longer loaded: styles for `.notyf__toast` and scripts that call `new Notyf()` need to change, or link Notyf themselves. `lib/notyf.min.js` and `lib/notyf.min.css` are no longer shipped; an upgrade leaves the old copies in place.
- The hooks and events get a string `message` and an object `data` even when the processing snippet left them out; `FetchIt.isResponse()` checks only the boolean `success`. An exception in a `FetchIt.Message` hook is logged with the name of the hook.
- Forms get the service fields of the protection right after the form tag: a hidden input and an off-screen `div` with the trap. They are removed from `$_POST` and `$_REQUEST` before FormIt reads them.
- Scripts other than the bundled one must send `fetchit_token` and take the next token from the `X-FetchIt-Token` header of every answer (see README).
- With the protection on, the snippet still runs FormIt on every page view (for its preHooks), but only a POST that carries a token of its form counts as a submission; any other POST is hidden from FormIt. A POST to the page URL used to reach FormIt directly.
- On MODX 3 the plugin and `action.php` take FetchIt from the service container instead of the deprecated `getService()`.
- `fetchit:error` also fires for failed requests, with `detail.response` set to `null`. Handlers that read `detail.response` must allow for it.
- The processing snippet gets only posted values in `fields`: `$_POST` (and `$_FILES` from `action.php`). Before it got `$_REQUEST`, which also holds GET values and, depending on `request_order`, cookies.
- `method` and `data-fetchit` now come last among the attributes of the form tag.
- The inline `create()` call checks that the class from `fetchit.frontend.js.classname` is loaded, so a page without the script falls back to a normal submit instead of throwing. The class may be a window property, a top-level `class` declaration or a dotted name.
- An exception in a `FetchIt.Message` hook is logged to the console and no longer stops the form from showing the answer.
- The `?v=` added to the script URL keeps an existing query string and fragment.
- `FetchIt.create()` warns in the console when no form matches the action; it used to do nothing.
- A second submit while a request is running is ignored.
- `fetchit:success` can be cancelled: `event.preventDefault()` keeps the fields filled and skips the reCAPTCHA reset. It was meant to work this way, but the event was not cancelable.

### Fixed

- FormIt 5.2 and later has an AJAX mode of its own. For every page view with a FetchIt form it stored the FormIt properties in the session (when there is one) and in a cache file kept for `session_gc_maxlifetime` (a week by default), for its `action.php`, which processes the form without the protection of FetchIt, and linked `formit.js` (with the default `formit.frontend_js`). For the FetchIt form, FetchIt now removes the stored properties, its `ajaxToken` placeholder (restoring the token of another FormIt form with the same prefix) and `formit.js` with its inline config; FormIt forms on the same page keep their AJAX mode. The reCAPTCHA of FormIt 5.2 gets its answer through `formit.js`, so on FetchIt forms use `fetchit.captcha` instead.
- A form sent without JavaScript never showed its success or error message when the chunk used output filters on the FormIt placeholders, like the example chunk: the chunk rendered before FormIt ran. The snippet now processes first.
- A form that already had a `data-fetchit` attribute came out broken: `<form\data-fetchit="$action" ...>`, with the attributes before it lost. The snippet also changed look-alike tags such as `<form-field>` and the `formmethod` or `data-fetchit` of elements inside the form. Form tags are now parsed one by one, values in quotes included (so `@submit="step > 1"` stays intact), and only their own `method` and `data-fetchit` are replaced.
- No scripts were added on sites without anonymous sessions. Whether a page has a form is now remembered for the current request, not in the session.
- No scripts were added when `<head>` was written in capitals; with attributes on `<head>` they went to its end instead of before the first script.
- FormIt was not installed together with FetchIt on MODX 3: the package provider there answers in a different format. The installer also reports a missing provider, an unreachable provider, an HTTP error, a failed download and a failed install separately, and removes a package it could not install.
- A FormIt error placeholder holding only `&nbsp;` marked the field as invalid.
- Without the `fetchit.frontend.js` setting the plugin linked the missing `js/default.js`.
- Two identical snippet calls on one page bound the form twice, and it was sent twice.
- Fields disabled in the markup became enabled after a submission.
- A network error or an answer that is not a FetchIt answer (a PHP error page, HTML after a redirect, JSON from a firewall) was only logged to the console. The visitor now sees the `fetchit_err_request` message.
- Field names with quotes broke the error selectors.
- A property set that does not exist (`&snippet=`FormIt@typo``) made `action.php` fail with a `TypeError` on PHP 8. The snippet now runs without it, and the MODX log names the missing set.
- PHP 8.1+ deprecation notices when the invalid-class settings are missing.

## [1.1.3] - 2026-08-12

### Fixed

- Fatal error `Serialization of 'PDO' is not allowed` on frontend with PHP 8.3+ (#17). Action properties are stripped of objects/resources before session/cache write; `$scriptProperties['FetchIt']` is no longer injected — use `$modx->getService('fetchit', 'FetchIt', MODX_CORE_PATH . 'components/fetchit/model/', [])`.
- Form-level success and validation messages now update in the form on AJAX submit via `[data-success]` and `[data-validation-error]` ([#11]). Custom templates need these attributes instead of `[[+fi.success:…]]` conditionals for AJAX.
- Whitespace-only FormIt field errors (e.g. `fi.error.email`) no longer mark the form as failed ([#10]). Placeholders are sanitized with `strip_tags` and `html_entity_decode`; blank `validation_error_message` falls back to the lexicon key.
- PHP 8 warning for undefined `$_SESSION['fetchit_called']` in `registerScript()` ([#7], [#9]).
- PHP 8 warnings for undefined `$_SESSION['FetchIt']` when saving or reading snippet properties.
- Dead `!isset($_POST)` check in `action.php` replaced with `empty($_POST)` so GET requests redirect correctly.
- Session/cache storage for snippet properties moved to `storeActionProperties()` / `loadActionProperties()`.
- Custom `successMessage` from the FetchIt snippet call is used in the AJAX success response ([#5], [#11]).

## [1.1.2] - 2023-11-11

### Added

- Added error for Recaptcha V3.

## [1.1.1] - 2023-07-14

### Changed

- Fixed support in php 8.

## [1.1.0] - 2023-06-12

### Added

- Default notifier.

## [1.0.1] - 2023-03-21

### Changed

- Fixed undefined fetchit bug when snippet calls by fenom.

## [1.0.0] - 2023-03-10

### Changed

- Fixed undefined variable $hash.

## [1.0.0-beta] - 2023-02-20

### Added

- First init.
