Skip to content

FetchIt 4.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 25 Sep 03:54
· 5 commits to master since this release

One package for MODX 2.8 and MODX 3, with spam protection on by default. It
installs over FetchIt 1.x on MODX 2 and FetchIt 3.x on MODX 3; system settings
and chunks are kept. What may affect your code is marked Breaking below and
explained in the README, Upgrading to FetchIt 4.

Added

  • Spam protection for every form, on by default, both for FetchIt submissions and for forms sent without JavaScript. fetchit.protection turns it all off.
    • A signed single-use token in every form (fetchit_token), with no session needed. Only a request with a well-signed token of the form gets the next one, in the X-FetchIt-Token header. When the token of a page is stale (a cached page, a page open for long, a new key), the script sends the form once more with the new token by itself.
    • A minimum fill time (fetchit.protection.min_time, 3 seconds), counted from the first output of the page, resends included.
    • A hidden trap field with a random name per installation. A bot that fills it gets a fake success, and the form is not processed.
    • A limit of submissions per form and client address (fetchit.protection.rate_limit, fetchit.protection.rate_window), also behind trusted proxies and CDNs (fetchit.protection.proxies, fetchit.protection.ip_header).
    • Used tokens are marked atomically in core/cache/fetchit/tokens/, which "Clear cache" leaves alone. When a mark cannot be written, the form is refused and the cause is logged.
    • An optional proof of work (fetchit.protection.pow, in bits, off by default): the browser finds n such that SHA-256 of token:n starts with that many zero bits, 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 captchas: Cloudflare Turnstile, Google reCAPTCHA v3 and Yandex SmartCaptcha (fetchit.captcha, fetchit.captcha.site_key, fetchit.captcha.secret_key, fetchit.captcha.min_score). FetchIt adds the provider's script, gets its answer before each submission and checks it on the server last. A provider that cannot be reached refuses the form with its own message, fetchit_err_captcha_unavailable; a captcha with no answer to send stops the submission with fetchit_err_captcha_client.
    • A log of refusals and problems (fetchit.protection.log), and a signing key generated at install (fetchit.protection.secret). Mistakes in the settings of the captcha and the proof of work are logged.
  • The OnFetchItBeforeProcess event: a plugin gets $action, $fields, $properties and $FetchIt, and refuses a submission with $modx->event->output(). It fires with the protection off too.
  • MODX 3 support in the same package, built on MODX 2.8: it installs fresh on MODX 2.8 and MODX 3.
  • The API of FetchIt 3.x on both versions: the FetchIt\FetchIt class, saveActionProperties() and getActionProperties(), and $modx->services->get('FetchIt') on MODX 3.
  • FetchIt::service(), one shared instance on MODX 2 and MODX 3; the 1.x and 3.x ways to get FetchIt return the same instance.
  • FetchIt::pdoTools(), which finds pdoTools 2 on MODX 2 and pdoTools 3 on MODX 3, for Fenom and @FILE chunks.
  • FetchIt::prepareForm(), which gives the form tags of a chunk the POST method and the key of the form.
  • TypeScript types, assets/components/fetchit/js/fetchit.d.ts: FetchIt, the config, FetchIt.Message, the instances of forms and the fetchit:* events with their detail. The script is type-checked against them.
  • FetchIt.createNotifier(): the built-in notifications for a site that sets FetchIt.Message itself. duration: 0 keeps a notification until it is closed.
  • detail.error in fetchit:error when a request fails, and the fetchit_err_request lexicon entry the visitor sees then.
  • MODX log entries when the script cannot be added to a page with a form: no <head>, or a fetchit.frontend.js that is not a .js file.
  • An upgrade with fetchit.frontend.default.notifier on says in its log that Notyf is no longer loaded, and warns when fetchit.frontend.js points to a script of the site.

Changed

  • Breaking: forms get the service fields of the protection right after the form tag; they are removed from $_POST before FormIt reads them, so they never reach e-mails. A script of the site used instead of the bundled one must send fetchit_token and take the next token from the X-FetchIt-Token header of every answer.
  • Breaking: the notifications of fetchit.frontend.default.notifier are FetchIt's own instead of Notyf, and window.Notyf is no longer loaded. Styles for .notyf__toast and scripts that call new Notyf() need to change. The new notifications are accessible (live regions for screen readers, a labelled close button, focus handling), stay while hovered or focused, and take CSS variables for their colours, by default the pastel green and red of Tailwind CSS 4. Under a Content-Security-Policy they take the nonce of the FetchIt script.
  • Breaking: fetchit:error also fires when a request fails, with detail.response set to null.
  • Breaking: the processing snippet gets only the form sent in fields: $_POST, and $_FILES for FetchIt submissions. It used to get $_REQUEST, with GET values and, depending on request_order, cookies.
  • With the protection on, the snippet still runs FormIt on every page view, for its preHooks, but only a POST with a token of its form counts as a submission.
  • The script is built with ES2019 syntax, so it runs in every browser of the project's browserslist.
  • The hooks of FetchIt.Message and the events get a string message and an object data even when the processing snippet left them out. An exception in a hook is logged with its name and no longer keeps the answer from the form.
  • fetchit:success can be cancelled: event.preventDefault() keeps the fields filled.
  • A second submission while a request is running is ignored.
  • On MODX 3 the plugin and action.php take FetchIt from the service container instead of the deprecated getService().
  • The inline FetchIt.create() call checks that the class of fetchit.frontend.js.classname is loaded, so a page without the script falls back to a normal submission instead of throwing. FetchIt.create() warns in the console when no form matches.
  • method and data-fetchit come last among the attributes of the form tag, and the ?v= of the script URL keeps its query string and fragment.

Removed

  • lib/notyf.min.js and lib/notyf.min.css are no longer shipped. An upgrade leaves the old copies in place.

Fixed

  • 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 was rendered before FormIt ran.
  • A form tag with its own data-fetchit came out broken, look-alike tags such as <form-field> were changed, and so were the formmethod and data-fetchit of elements inside the form. Form tags are now parsed one by one, quoted values included.
  • The script was not added on sites without anonymous sessions, and when <head> was written in capitals or had attributes.
  • FormIt was not installed together with FetchIt on MODX 3. The installer now also reports each way the download or install of FormIt can fail.
  • 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 FetchIt's (a PHP error page, HTML after a redirect, JSON from a firewall), was only logged to the console. The visitor now sees fetchit_err_request.
  • A FormIt error placeholder holding only &nbsp; marked its field as invalid.
  • Field names with quotes broke the error selectors.
  • A snippet property set that does not exist made action.php fail on PHP 8. The snippet now runs without it, and the MODX log names the missing set.
  • Without the fetchit.frontend.js setting the plugin linked the missing js/default.js.
  • PHP 8.1 deprecation notices when the invalid-class settings are missing.

Security

  • A POST straight to the URL of a page with a FetchIt form reached FormIt and its hooks, e-mails included, without any check. It now has to carry a valid token of the form.
  • FormIt 5.2 and later kept the properties of every FetchIt form, hooks included, for its own action.php, which processes a form without FetchIt's checks. FetchIt now turns that AJAX mode off for its forms: it removes the stored properties, the ajaxToken placeholder and formit.js. FormIt forms of their own on the same page keep their AJAX mode.