{"version":"2.0.24","date":"2026-09-03T20:39:28Z","assets":{"grav-admin":{"name":"grav-admin-v2.0.24.zip","type":"binary\/octet-stream","size":23112034,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/2.0.24","content_sha256":"d470152707ad2e8d353b4cc8f7acecbc2ca286a02e20914ac2731af98d50c170","sha256":"8f44dda40dac243c2ea538e85040f139fbebb58dd812f138c9e205fd779e7042"},"grav-update":{"name":"grav-update-v2.0.24.zip","type":"binary\/octet-stream","size":8349754,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/2.0.24","content_sha256":"a190a0b047099d023411e01c2e5a3240a153325f76a2d0adaae46589ac6eec4f","sha256":"cf2f3761ff039a45415c260338d9309ed1427c2436bdec48f74a984e0a84038e"},"grav":{"name":"grav-v2.0.24.zip","type":"binary\/octet-stream","size":10209678,"download":"https:\/\/getgrav.org\/download\/core\/grav\/2.0.24","content_sha256":"7cc47cce425c8e0a0fc108a003c6d9a38d95341491250d53ae7005fa1dacc1a8","sha256":"d1cb3c52e12d8f3b3e35c94c2c6386a17554264161e59fb57176ef1698743c57"}},"url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/2.0.24","min_php":"7.2.3","changelog":{"2.0.24":{"date":"09\/03\/2026","content":"1. [](#new)\n    * **A dependency can now name the generation of Grav it is for.** A plugin that supports both 1.7 and 2.0 often needs a different version of the same dependency on each, so a `dependencies` entry takes an optional `grav` key: `- { name: form, version: '>=9.1.0', grav: '2.0' }`. Entries without it apply everywhere, so existing blueprints are unchanged. See [Plugin Compatibility](https:\/\/learn.getgrav.org\/20\/plugins\/plugin-compatibility#requiring-different-versions-per-grav-generation)\n    * A new `system.images.progressive_jpeg` setting, on by default, controls whether resized and cached JPEGs are saved as progressive\n\n1. [](#bugfix)\n    * Installing a package whose dependency is not in the GPM index now says so and carries on, instead of stopping the command with a PHP fatal error. A plugin that still asks for the Grav 1.7 admin plugin was enough to trigger it [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * A plugin that asks for the `admin` plugin now has that read as Admin 2 on Grav 2, so a plugin written for both 1.7 and 2.0 installs instead of failing on a dependency that cannot exist there. The version it asks for is not carried over, because it describes the old admin's numbering [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * Any other dependency that cannot be installed on this generation of Grav is now left out of the install rather than attempted and failed\n    * A JSON request whose body is a bare scalar (`\"text\"`, `12345`, `true`) sent with `Content-Type: application\/json` no longer answers a 500 from the request pipeline before any route runs. It is treated as an empty body, so a plugin's webhook or API route gets to answer it, log it and refuse it itself. With `errors.display` on, the old failure also printed a stack trace with server paths to whoever sent it\n    * An image default such as `resize` set in `system.images.defaults` works again. Every image manipulation was being skipped since 2.0.22, leaving only the loading and decoding hints [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Settings such as `loading: lazy` now stay on the image when `link` is also on, instead of moving onto the surrounding link where the browser never sees them [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Resized and cached JPEGs are saved as progressive again, so a photo appears as a whole blurry image that sharpens instead of filling in one line at a time. It has been Grav's default since 2014 but silently stopped working in 1.4.6 [#4284](https:\/\/github.com\/getgrav\/grav\/issues\/4284)\n    * A media file's own settings from `media.yaml` are no longer overwritten the moment the image is opened, so custom default filters for an image type work again\n    * A URL typed with a trailing slash no longer errors out on a site that has the debugger switched on and the Login plugin protecting page media, because the debug bar is now skipped on redirects, where there is no page to put it on [#4280](https:\/\/github.com\/getgrav\/grav\/issues\/4280)\n    * With the optional `system.session.read_and_close` setting turned on, a change made to the session early in a request is no longer thrown away by a later write in that same request, and a message added just before a redirect now reaches the page it was meant for [#4281](https:\/\/github.com\/getgrav\/grav\/issues\/4281)\n"},"2.0.23":{"date":"09\/02\/2026","content":"1. [](#improved)\n    * `composer.json` now declares the `ctype` and `session` extensions it has always used, and suggests `fileinfo` and `simplexml`. Installing with `composer create-project` on a machine missing one of these no longer quietly walks back to a years-old release of Grav instead of failing [#4273](https:\/\/github.com\/getgrav\/grav\/discussions\/4273)\n1. [](#bugfix)\n    * Reading the browser name, platform or version no longer raises a PHP deprecation notice when a request arrives with no user agent, which is every request from a bare script or a health check\n    * Opening Clockwork before anything has been profiled, on a fresh install or right after `bin\/grav clear`, now reports that there is no data yet instead of failing with a 500\n    * A `GRAV_CONFIG__` override set to `true` or `false` now reaches the configuration as a real yes\/no value instead of the word itself, so switching something off from a `.env` file or the server environment actually switches it off. Thanks to @nerdyjan for the report and @AdilAzhariOmsan for the fix [#4277](https:\/\/github.com\/getgrav\/grav\/issues\/4277)\n    * `Uri::ip()` now reads the visitor's address from `$_SERVER`, falling back to the environment, so hosts that don't hand request variables to PHP's environment no longer report every visitor as `UNKNOWN`. Anything that counts per address there, such as the Login plugin's failed-login lockout and per-IP rate limiting, had been sharing a single bucket. Thanks to @sandymac [#2507](https:\/\/github.com\/getgrav\/grav\/issues\/2507)\n    * On those same hosts the `system.http_x_forwarded` options for `ip`, `client_ip` and `cf_connecting_ip` had no effect at all, and now work as documented. If you turned one on and saw nothing change, turn it back off unless the site really is behind a proxy that overwrites that header\n"},"2.0.22":{"date":"08\/31\/2026","content":"1. [](#new)\n    * The `url()` Twig function now takes a language, so a link to a route that isn't a page - a search page, a form action - can carry the site's language prefix: `{{ url('\/search', lang=true) }}`\n    * Blueprints can use a `media` field type, which saves a picked file as its path and keeps a list of them when the field allows more than one\n    * Page collections can now exclude one or more template types with `notOfType()`, the counterpart to the existing `ofType()` [#3910](https:\/\/github.com\/getgrav\/grav\/issues\/3910)\n    * The scheduler can now run the jobs that have missed their scheduled time, rather than only the ones due this very minute. Run it with `bin\/grav scheduler --catch-up`, which is what you want on a site that has no cron entry set up\n1. [](#improved)\n    * Twig in page content can no longer read the site's configuration through the `print_r`, `vardump`, `json_encode`, `yaml_encode` and `string` filters. The check that was meant to stop it had been asking whether the whole site was in sandbox mode, which Grav never does \u2014 it decides per template \u2014 so it had been letting everything through. Thanks to @Vectrain51\n    * The `|map`, `|filter` and `|reduce` Twig filters now check for themselves whether the template calling them is sandboxed, rather than relying on Twig to work it out. Thanks to @DhiyaneshGeek\n    * The scheduler no longer rebuilds a queued job from an unsigned queue file, so a file written into the queue folder by something other than Grav can at most re-run a job the site was already set up to run. The page index also refuses to build objects while reading its cache. Thanks to @elite0529\n    * Plugin and theme descriptions are now rendered in Parsedown's safe mode before admin displays them, so a description carrying raw HTML shows as text rather than being rendered. Thanks to @alham-rizvi\n    * The debugger's Clockwork data endpoint now answers only requests coming from the server itself, or requests presenting the secret set in the new `debugger.token` option. Cookies and API tokens are no longer recorded in profiler data whatever the `censored` option is set to\n    * Building a URL is now faster, which adds up over the hundreds of asset and link URLs a single page render produces\n    * The scheduler now records whether a run was started by cron or by hand, and a run you started yourself no longer counts as evidence that cron is set up\n    * `bin\/grav scheduler -r` now records the run against each job, the same as a scheduled run, so the next run knows what has already happened\n    * A field that is rejected only for being too long or too short now says so, and gives both the length submitted and the limit, instead of the same \"Invalid input\" any other bad value gets\n    * Multiline fields no longer carry a length limit low enough to affect real writing. Set `max: 0` on a field to remove the limit altogether\n1. [](#bugfix)\n    * The `|reduce` Twig filter now actually reduces. It was running the `|map` code by mistake and throwing away the starting value, so `[1,2,3]|reduce((c, v) => c + v, 0)` gave back a list instead of `6`. Thanks to @DhiyaneshGeek\n    * A form field's `minlength` and `maxlength` are now checked when the form is submitted, not only by the browser. They were being written into the page as HTML attributes but ignored on the server, so anything that skipped the browser's own check went straight through [#642](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/642)\n    * A field with a `step` set now accepts the lengths and counts that land on a step, and rejects the ones that do not. The check was the wrong way round, so it rejected exactly the values it was meant to allow\n    * A long page can be saved from the admin again. Page content was capped at 65,536 characters, so anything longer than roughly twenty pages of text was refused, and the only way to edit it was to write the file directly [#3643](https:\/\/github.com\/getgrav\/grav\/issues\/3643)\n    * A site installed in a subfolder no longer mangles URLs whose path repeats the install folder's name, such as an image at `\/images\/subdir\/photo.jpg` on a site installed at `\/subdir`\n    * A link to a page that carries a query string or an anchor, such as `\/blog?page=2`, now resolves to the page and keeps its language prefix, instead of being passed through as a plain path\n    * On a site installed in a subfolder, links written with the full path now resolve to the page, so they pick up the site's language and page extension\n    * A cache folder that the web server cannot write to no longer takes the whole site down. Grav now logs a warning naming the folder and serves the request without the cache, so the front end and the admin both stay reachable and the Problems plugin can report what is wrong [#4260](https:\/\/github.com\/getgrav\/grav\/issues\/4260)\n    * The same failure writing `user\/config\/versions.yaml` no longer stops the site either [#3688](https:\/\/github.com\/getgrav\/grav\/issues\/3688)\n    * Errors about a file that cannot be written now name the folder and say whether it is missing or not writable, instead of only reporting the file\n    * A session cookie name starting with `__Secure-` or `__Host-` now keeps that prefix and is sent with the settings browsers require for it, so the extra protection those prefixes give actually applies. Thanks to @wakqasahmed for the fix [#3773](https:\/\/github.com\/getgrav\/grav\/issues\/3773)\n      Note: sites whose `system.session.name` contains capitals, underscores or a leading or trailing dash will get a slightly different cookie name after this update, which signs their users out once.\n    * The scheduler no longer reports that cron is not set up when the crontab entry is written in a valid but slightly different style, such as one using `&&` or an absolute path to `bin\/grav`\n    * On a site with a custom scheduler job of its own, looking up a job by name no longer misses every job the system and its plugins register, so the backup and cache jobs can be found and run individually\n    * A scheduler job that finished its work but could not then write its output file, send its notification email or run its callback no longer aborts the whole run. The remaining jobs run, every result is still recorded, and the problem is written to the log\n    * A scheduler job registered without a schedule of its own no longer causes an error when its next run time is worked out\n    * `bin\/grav scheduler -j` no longer fails on a site with jobs registered by a plugin, and `-d` no longer fails on a job that has never run\n    * A scheduler job that runs one of Grav's own command line scripts now works when the scheduler is triggered from the web rather than from cron. Those jobs used to fail with \"env: php: No such file or directory\", because the web server does not have php on its path\n    * A scheduler job registered as a whole command line, such as `bin\/plugin myplugin sync`, now runs. Only the executable and its arguments given separately used to work, so a job written the other way looked for a file whose name contained spaces and failed every time it ran\n    * A site served from a subpath by a proxy no longer loses that subpath when a trailing slash is redirected, which previously sent visitors outside the site. The homepage of such a site also no longer redirects to the bare domain. Thanks to @wakqasahmed for the fix [#3822](https:\/\/github.com\/getgrav\/grav\/issues\/3822)\n    * With `force_ssl` turned on, a page that does not exist now redirects to HTTPS like every other page, instead of serving the 404 over plain HTTP. Thanks to @wakqasahmed for the fix [#3703](https:\/\/github.com\/getgrav\/grav\/issues\/3703)\n    * Image settings are no longer applied to audio, video, SVG or document media. An embedded MP3 kept its player instead of being turned into a linked thumbnail, and media URLs no longer pick up stray `loading`, `decoding` and `fetchpriority` values, which happened on every site whether or not those settings had been changed. Thanks to @wakqasahmed for the fix [#4264](https:\/\/github.com\/getgrav\/grav\/issues\/4264)\n"},"2.0.21":{"date":"08\/22\/2026","content":"1. [](#bugfix)\n    * Form fields no longer print their HTML attributes as text above the field, a problem the Twig update in 2.0.20 introduced on every form [#4256](https:\/\/github.com\/getgrav\/grav\/issues\/4256)\n    * A custom text escaper registered by a plugin now works again, instead of stopping the page with an error the first time a template used it\n"},"2.0.20":{"date":"08\/21\/2026","content":"1. [](#improved)\n    * Updated the bundled Twig fork to the current 3.x, picking up the correctness and sandbox improvements from the 3.27 and 3.28 releases.\n    * Grav now runs on Twig 3.28 and newer, which tightened the escaping method that Grav's compatibility shim replaces and would otherwise stop the site with a server error.\n    * The Twig content sandbox now accepts the list of tests a template uses, which newer Twig versions hand to it and will require from Twig 4.\n    * The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.\n    * Script and style files whose name already contains a version, such as those the Admin panel ships, are cached permanently in that same configuration, because a change always produces a new name.\n1. [](#bugfix)\n    * [security] Page content can no longer register a script or stylesheet through the Twig content sandbox, and asset URLs are now escaped where the tag is built, closing a way to inject markup into a rendered page.\n    * [security] The `read_file` capability no longer includes the user data folder by default, so page content can no longer be used to publish form submissions and other stored data.\n    * [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.\n    * [security] Custom Twig sandbox denial rules now take effect regardless of how the class name is capitalised, and can no longer be silently bypassed through a parent class or interface.\n    * A damaged page cache file is now rebuilt from the original page instead of stopping the site with a server error [#4239](https:\/\/github.com\/getgrav\/grav\/issues\/4239)\n    * Images and links in page content now work when the file name contains a colon, such as a screenshot named after a timestamp [#3933](https:\/\/github.com\/getgrav\/grav\/issues\/3933)\n    * A page that sets a full web address as its canonical route now uses that address on its own, instead of joining it onto the site's own address and breaking sitemaps and canonical links [#4023](https:\/\/github.com\/getgrav\/grav\/issues\/4023)\n    * Turning on asset timestamps now gives each stylesheet and script its own marker taken from when that file last changed, so editing one file no longer waits on an unrelated change before visitors see it [#4049](https:\/\/github.com\/getgrav\/grav\/issues\/4049)\n    * A canonical route set through the Flex pages API is now saved as written, instead of being stored in a form it could never be read back from.\n    * Flex directory blueprints no longer lose the fields the Flex Objects plugin adds when something reads the directory early in a request [#160](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/160)\n    * The scheduler's generated cron command now names the site's environment when that environment has its own configuration, and each run records which environment it used, so custom jobs defined in `user\/env\/<host>\/` no longer fail silently from cron [#4248](https:\/\/github.com\/getgrav\/grav\/issues\/4248)\n    * Audio and video players generated by `media.html()` no longer carry an `alt` attribute, which isn't valid on those elements; any alternative text is kept as an accessible label instead, so the markup passes validation [#3540](https:\/\/github.com\/getgrav\/grav\/issues\/3540)\n"},"2.0.19":{"date":"08\/14\/2026","content":"1. [](#new)\n    * You can now tighten the Twig content sandbox below its built-in defaults with new `denied_*` settings in `security.yaml`.\n    * The \"Twig in Content\" report can show the effective sandbox policy, so you can see exactly what page content is allowed to do.\n1. [](#improved)\n    * A theme or plugin that ships its own `.htaccess` can no longer switch off the protection on its own folder, which used to leave its configuration and template files downloadable [#4236](https:\/\/github.com\/getgrav\/grav\/issues\/4236)\n    * Twig in page content now renders on new installs by default, instead of appearing as raw text until the setting was turned on.\n    * The long Twig sandbox allowlists now ship built into Grav, so `security.yaml` only records your own additions and future security updates to the defaults reach every site.\n    * Existing sites that had trimmed those allowlists to tighten them keep exactly that policy after upgrading, now recorded as explicit `denied_*` entries.\n    * Removed two rarely-used Twig sandbox switches (`logging` and `admin_hint`); both behaviours are now always on.\n1. [](#bugfix)\n    * The content cross-site scripting check no longer objects to harmless `<option>` and `<select>` markup, whose original issue is fixed in the form field that actually rendered it.\n    * Sites running with the Twig 2 compatibility setting no longer crash with a server error on every page once an update clears the template cache [#4235](https:\/\/github.com\/getgrav\/grav\/issues\/4235)\n"},"2.0.18":{"date":"08\/11\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.14, which closes two further ways a crafted stylesheet could hide an external image reference from the checks added in the previous release ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n    * [security] Modular pages are now checked for cross-site scripting when they are saved, closing a way for a page editor to store a script that ran for every visitor ([GHSA-fg8g-663r-f366](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fg8g-663r-f366)).\n    * [security] The Twig `sort` and `find` filters no longer run a plain function name as a callable inside the content sandbox, closing a way for a page editor to execute arbitrary PHP ([GHSA-p6qj-p5m7-f62h](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p6qj-p5m7-f62h)).\n    * A blueprint that builds on another one can again fill dropdowns from its own PHP, which mostly affected themes because their page blueprints nearly always extend the default one ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)).\n    * The Scheduler no longer fails outright on hosts that disable PHP's `proc_open`, so scheduled jobs can still be viewed and edited there [getgrav\/grav-admin-next#16](https:\/\/github.com\/getgrav\/grav-admin-next\/issues\/16)\n    * A scheduled job that cannot be started on such a host is now reported as failed with an explanation, instead of stopping the whole scheduler run\n    * Grav now works out who the site runs as without starting a shell, so that detail still appears when external commands are unavailable\n    * The record of when the scheduler last ran is now written to a fixed location rather than one relative to wherever the trigger happened to run from\n1. [](#improved)\n    * Grav now decides whether the scheduler is being triggered by checking that each job has run when its own schedule says it should have, instead of requiring a run in the last two minutes, so a sparse crontab, a webhook or a scheduled task on Windows all count\n"},"2.0.17":{"date":"08\/07\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.13, which stops CSS comments from hiding dangerous values and covers image loading through `image-set()`, so untrusted SVG or HTML can no longer reference external resources those ways ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n"},"2.0.16":{"date":"08\/07\/2026","content":"1. [](#improved)\n    * Updated vendor libs to latest versions\n    * The list of groups on the account form is now read through the current user groups system, retiring a routine deprecated since Grav 1.7.\n1. [](#bugfix)\n    * [security] The site, system and theme settings offered to Twig written inside page content are now filtered by the same denied-paths list that already covered `config`, so a page editor can no longer read secrets such as a Redis password straight out of them ([GHSA-p597-crqc-m349](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p597-crqc-m349)).\n    * [security] Twig written into a form's email settings now runs under the same restrictions as Twig written into page content, closing a route that let someone with only page-editing rights run commands on the server ([GHSA-gh8j-q67c-j53f](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-gh8j-q67c-j53f)).\n    * [security] Form security tokens are now compared with a routine that takes the same amount of time whichever characters differ, so the check can no longer hint at how much of a guess was right ([GHSA-38p6-h87p-r4cg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-38p6-h87p-r4cg)).\n    * [security] The check for whether a visitor arrived from your own site now requires a full address match, so another site whose domain merely begins with yours no longer counts as your own ([GHSA-9ccq-2jfg-qw33](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-9ccq-2jfg-qw33)).\n    * [security] Scheduler job locks are now kept inside your site rather than in the shared system temp folder, so another account on the same server can no longer redirect a lock write to a file of its choosing ([GHSA-q8w8-6cq5-j4h2](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q8w8-6cq5-j4h2)).\n    * A scheduled job set to run only one at a time now refuses to start when its lock cannot be written, instead of running unprotected. Note that `bin\/grav clear` removes these locks along with the rest of the temporary folder.\n    * [security] Deleting, renaming and copying a media file now check the whole path rather than just the file's own name, so a plugin calling those routines directly cannot reach a file outside the media folder ([GHSA-jq29-c7v8-rg55](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-jq29-c7v8-rg55)).\n    * [security] The `media_directory()` Twig function now only accepts folders inside your site, so Twig written into page content can no longer list files or republish images from elsewhere on the server ([GHSA-47ch-6w46-6xm7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-47ch-6w46-6xm7)).\n    * Deleting a media file no longer also removes the retina copies and metadata belonging to a different file whose name merely ends with the same text, so deleting `banner.jpg` leaves `my-banner@2x.jpg` alone.\n    * A fallback page handed to `Uri::referrer()` is now returned as given, instead of being trimmed away to nothing whenever the visitor did not arrive from your site.\n    * `Pages::referrerRoute()` was comparing a full web address against a site path, so it never recognised a match and always reported that the visitor came from somewhere else.\n"},"2.0.15":{"date":"08\/03\/2026","content":"1. [](#bugfix)\n    * A plugin's blueprints can use the data providers that plugin ships again, instead of having every one of them refused by a check that only recognised the providers core itself registers ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)). Fields defined in page frontmatter are held to the stricter rule instead, which is where the risk actually was.\n    * [security] A configuration admin can no longer reach an unvetted built-in routine by writing a blueprint field's data provider as a plain function name, a spelling that was still being checked against a list of known-bad names rather than the list of approved ones ([GHSA-f8wv-xp27-6gq7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-f8wv-xp27-6gq7)).\n    * [security] The content security scan now reads an unpaired quote inside an unquoted attribute value the way a browser does, closing another way a page editor could hide a script from it ([GHSA-vfmf-q6x9-cw96](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-vfmf-q6x9-cw96)).\n    * [security] The media URL in an audio or video tag is now escaped, so a filename carrying markup can no longer add its own attributes to the player ([GHSA-6qw9-4vv5-jr97](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-6qw9-4vv5-jr97)).\n    * JSON responses no longer fail outright when the data contains invalid UTF-8. `json_encode()` returns `false` on malformed bytes, and the PSR-7 response body is type-hinted `string|resource|StreamInterface`, so that `false` came back out as an unhandled `TypeError` from inside the vendor stream rather than as a response. Affected `createJsonResponse()` and both JSON error responses in `ControllerResponseTrait`, where an exception message carrying a bad byte would take out the error handler itself, plus the Clockwork data endpoint in `Debugger`. Bad bytes are now substituted, and the remaining structural failures (recursion depth, `INF`\/`NAN`) raise a catchable `JsonException` instead of a silent `false`. Output for valid data is unchanged.\n    * [security] The fast static asset server now keeps a request inside the directory the site published, instead of also allowing any neighbouring directory whose name starts with the same letters ([GHSA-4v9q-p283-qc2m](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-4v9q-p283-qc2m)).\n    * [security] File uploads now reject a few more extensions that browsers run script from, or that a server may hand to PHP: `xhtml`, `xht`, `svgz`, `php7`, `php8`, `pht`, `phtm` and `phps` ([GHSA-66xf-ggf4-6hmc](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-66xf-ggf4-6hmc)).\n    * [security] The bundled `Caddyfile` protections did nothing. They were written as nginx-style regexes, which Caddy reads as literal paths that never match, and the `respond` they redirected to ran after the catch-all rewrite had already claimed the request. A site served with this config handed out `user\/accounts\/`, `user\/config\/`, `logs\/`, `composer.lock`, page files, and the `system\/` and `vendor\/` folders to anyone who asked. The rules are now named `path_regexp` matchers answering `403` directly, inside a `route` block so they run before the rewrite, and they were checked request by request against the `.htaccess` behaviour.\n"},"2.0.14":{"date":"07\/30\/2026","content":"1. [](#improved)\n    * Removed old JQuery `2.1.4`. However `2.2.4` and `3.7.1` remain for legacy support\n1. [](#bugfix)\n    * [security] An administrator with account-management rights can no longer grant themselves super-admin access by saving it into a user group's permissions, a protection the account form already had ([GHSA-xhfv-7758-r9hx](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xhfv-7758-r9hx)).\n    * [security] The content security scan no longer reports a page as clean when it was unable to examine it, closing two ways a page editor could hide a script from it: a single invalid character anywhere in the content, and padding a tag out to several thousand characters ([GHSA-q2j8-x8hf-63ch](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q2j8-x8hf-63ch)).\n"},"2.0.13":{"date":"07\/25\/2026","content":"1. [](#new)\n    * Added an `array_group_by` Twig filter and function for grouping a list of items by one of their values ([#4218](https:\/\/github.com\/getgrav\/grav\/pull\/4218)).\n1. [](#bugfix)\n    * [security] A configuration admin can no longer run code on the server by pointing a dynamic field's data provider at a built-in routine named as a class-and-method pair, a form that slipped past the safety check because it only inspected the single-string spelling; both forms are now vetted the same way ([GHSA-r94f-hx44-8jqf](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-r94f-hx44-8jqf)).\n    * [security] A page editor without super-admin rights can no longer store an event-handler script that runs for site visitors by hiding it behind a `>` placed inside a quoted HTML attribute; the content security scan now reads quoted attribute values the same way a browser does ([GHSA-269c-h76q-8cxw](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-269c-h76q-8cxw)).\n    * [security] A backup profile's location is now confined to the site folder, so a profile pointing outside the Grav root can no longer pull external directories into the backup archive ([GHSA-fch7-cpv4-w7hg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fch7-cpv4-w7hg)).\n    * [security] Uploaded filenames may no longer contain the HTML characters `<`, `>`, or `\"`, so a stored filename cannot carry markup that could run if it were later shown unescaped.\n    * [security] The `find` and `sort` Twig filters now reject a dangerous function name given as their callback, matching the protection already applied to `map`, `filter`, and `reduce`, so template values cannot use them to run code ([GHSA-xx48-97m4-h7qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xx48-97m4-h7qm)).\n    * The bundled `nginx.conf` security rules are now anchored to the start of the path like the `.htaccess` rules already are, so the admin's Tools \u2192 Logs viewer works on nginx instead of being blocked ([#4223](https:\/\/github.com\/getgrav\/grav\/pull\/4223)).\n    * On non-FastCGI setups Grav no longer sends an invalid `Content-Encoding: none` header, which some strict HTTP clients rejected outright; it now closes the connection cleanly without the bogus value ([#2619](https:\/\/github.com\/getgrav\/grav\/issues\/2619)).\n"},"2.0.12":{"date":"07\/20\/2026","content":"1. [](#new)\n    * Added per-language fallbacks for unsupported browser languages during `HTTP_ACCEPT_LANGUAGE` negotiation, allowing them to resolve to supported languages without exposing additional language routes.\n1. [](#improved)\n    * The `read_file()` Twig function can now read `.css` files by default, so inline stylesheets can be embedded in a template without adjusting the security config ([#4215](https:\/\/github.com\/getgrav\/grav\/issues\/4215)).\n    * Documented that the site-wide media object resolves lazily, so its inherited collection query methods need `media_directory()` to filter or sort site media ([#4210](https:\/\/github.com\/getgrav\/grav\/issues\/4210)).\n1. [](#bugfix)\n    * Browser language codes are now matched case-insensitively during `HTTP_ACCEPT_LANGUAGE` fallback negotiation.\n"},"2.0.11":{"date":"07\/13\/2026","content":"1. [](#new)\n    * You can now filter, sort, and group a page's media by the values in their `.meta.yaml` metafiles directly in Twig, with new `filterBy`, `where`, `findBy`, `sortBy`, `groupBy`, and `withMeta` methods on `page.media`. Fixes [getgrav\/grav#4200](https:\/\/github.com\/getgrav\/grav\/issues\/4200).\n1. [](#bugfix)\n    * [security] A page editor can no longer read arbitrary files from the server by pointing an image watermark at a traversal path such as `carrier.png?watermark=..\/secret.png`; an editor-supplied watermark path is now confined to the site's media, while operator-configured watermarks and stream URIs are unaffected ([GHSA-w3f4-8pj2-599w](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-w3f4-8pj2-599w)).\n    * [security] A page-edit account can no longer reach file-disclosure or secret-read functions by naming an arbitrary `Class::method` as a dynamic field's data provider; qualified providers are now limited to a known-safe allowlist, closing a bypass of the guard added in 2.0.7 and 2.0.9 ([GHSA-7pgq-cr25-xvc8](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-7pgq-cr25-xvc8), [GHSA-cxv3-5jj3-cpgr](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-cxv3-5jj3-cpgr)).\n    * A page is no longer blanked when viewed just because a trusted plugin or shortcode on it outputs markup the content security scan flags, such as an embed, form, or icon; the check that guards against dangerous editor content now runs once when the page is saved rather than every time it is rendered ([GHSA-2c4f-86xc-cr74](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-2c4f-86xc-cr74)).\n1. [](#improved)\n    * [security] Page content that uses Twig to assemble disallowed markup at render time, such as building an event handler or a `<script>` tag from separate pieces, is now refused when you save the page instead of being allowed through to visitors.\n    * The `raw` Twig filter is no longer allowed inside editor-authored page content, so page content can no longer output unescaped dynamic values past the content security check; trusted theme templates are unaffected.\n"},"2.0.10":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * A partial `system.pages.process` override in `user\/config\/system.yaml` (for example setting only `twig: false`) no longer silently turns off Markdown and leaves every page rendering its raw source. Because `pages.process` is a single field in the blueprint, a partial override replaced the whole map and dropped the default `markdown: true`; core now re-applies that default, so an affected site recovers on update with no change to its config. An explicit `markdown: false` is still honored.\n"},"2.0.9":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * [security] A callable directive hidden in a Flex directory's blueprint field can no longer run commands on the server; dynamic field data on Flex types (Flex Pages, Flex Users, Flex Objects) now refuses dangerous functions, closing a path that bypassed the same guard added for page forms in 2.0.7 ([GHSA-c4wf-2xxc-68qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-c4wf-2xxc-68qm)).\n    * [security] The scheduler webhook trigger now fails closed when no token is configured, so an enabled webhook left without a token can no longer run scheduled jobs for anonymous callers ([GHSA-xwv3-2mv2-w33x](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xwv3-2mv2-w33x)).\n    * Images and links whose filename contains spaces now render when the path is wrapped in angle brackets, for example `![](<My image.jpg>)`. Fixes [getgrav\/grav#4197](https:\/\/github.com\/getgrav\/grav\/issues\/4197).\n"},"2.0.8":{"date":"07\/06\/2026","content":"1. [](#bugfix)\n    * An email or `www.` URL used as the visible text of a Markdown link is no longer turned into a second, nested link when GFM autolinks are enabled. Fixes [getgrav\/grav#4191](https:\/\/github.com\/getgrav\/grav\/issues\/4191).\n    * `md5()` can once again be called as a Twig function, not just as the `|md5` filter, so themes and plugins that generate an id or cache-busting hash with `md5(...)` keep working instead of failing with an \"Unknown function\" error. Fixes [getgrav\/grav-theme-quark2#12](https:\/\/github.com\/getgrav\/grav-theme-quark2\/issues\/12).\n    * Replacing an image in place, such as swapping the image in a Flex object field, now shows the new image on the site instead of the previously cached version (updated `getgrav\/image` to v4.1.3, which includes the source file's modification time and size in the derivative cache key). Fixes [getgrav\/grav#4195](https:\/\/github.com\/getgrav\/grav\/issues\/4195).\n"},"2.0.7":{"date":"07\/04\/2026","content":"1. [](#bugfix)\n    * [security] A page editor can no longer run commands on the server by hiding a callable directive in a form field's settings; dynamic field data now refuses dangerous functions and cannot be tricked into reaching one through a helper ([GHSA-fj2p-qj2f-74v5](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fj2p-qj2f-74v5)).\n    * A page's `translatedLanguages()` now localizes ancestor slugs too, so a nested translation whose parent folder has a localized `slug:` produces the fully translated cross-language link instead of leaving parent segments in the current language. Fixes [getgrav\/grav#4186](https:\/\/github.com\/getgrav\/grav\/issues\/4186).\n    * Pointing the log stream at `environment:\/\/` (for example `log: environment:\/\/logs`) no longer crashes the site or `bin\/grav clear` with a \"stream must either be a resource or a string\" error when the per-environment folder does not exist; logging now falls back to the default `logs\/` folder instead. Fixes [getgrav\/grav#4172](https:\/\/github.com\/getgrav\/grav\/issues\/4172).\n    * The `media:\/\/` stream now checks the per-environment `user\/env\/<host>\/media\/` folder before the shared `user\/media\/`, so site media stored per environment resolves to the correct URL in the admin and in page content instead of a broken `user\/media\/` link. Fixes [getgrav\/grav#4188](https:\/\/github.com\/getgrav\/grav\/issues\/4188).\n    * Large file downloads such as site backups are now streamed to the browser in chunks instead of being loaded into memory all at once, so a download bigger than PHP's memory limit no longer fails with a blank server error. Fixes [getgrav\/grav-plugin-api#12](https:\/\/github.com\/getgrav\/grav-plugin-api\/issues\/12).\n    * Backups initialization no longer runs twice when something that bypasses the normal request middleware (such as the API plugin) also attaches the backup scheduler listener, so the listener is registered only once.\n    * Pages accessed with URL parameters such as pagination or taxonomy filters no longer recompile every Twig template on each request, restoring full template caching on exactly the pages that get the most traffic.\n    * The per-file compiled cache for YAML and markdown files now loads through its intended opcache fast path, and a source file that has been deleted no longer serves stale cached data.\n    * A modular page that outputs trusted theme or plugin markup, such as a form with a reCAPTCHA field, is no longer wrongly blanked by the content security scan, which now checks the editor's own content instead of the finished template output. Fixes [getgrav\/grav-plugin-form#636](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/636).\n    * Chaining media actions on page media under the content security scan, such as `{{ page.media['x.jpg'].lightbox(1024,768).cropResize(176,176).html() }}`, now works instead of being blocked, and the scan's list of allowed media methods stays in step with Grav's documented media actions automatically.\n1. [](#improved)\n    * Updating a plugin or theme whose required dependency is held back by a newer Grav or PHP requirement now explains the real fix. Instead of reporting that the needed version is \"higher than the latest release\" and suggesting a cache refresh, the updater names the newer dependency release and the Grav (or PHP) version it needs, so you know to update Grav first. Relates to [getgrav\/grav-plugin-admin2#93](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/93).\n    * Backup profiles now always appear in the scheduler where each can be switched on or off with the Enabled\/Disabled toggle, instead of a profile staying hidden until its schedule was turned on; the profile's schedule setting seeds the default state and an explicit toggle takes precedence.\n    * Frontend requests are noticeably faster across the board: the scheduler, backups machinery, error page renderer and logger now initialize only when actually used instead of on every page view, cutting over 50 PHP files from a typical request.\n    * The filesystem scan that checks pages for changes now reuses its result for a couple of seconds (configurable with `cache.check.interval`), so busy sites no longer stat every page file on every single request; content edits still show up right away in normal editing workflows and admin saves remain instant.\n    * Configuration, blueprint and language file lists honor the same freshness window instead of checking every tracked file's timestamp three times per request, and theme blueprints no longer load at all on normal frontend page views.\n    * Class autoloading is faster: source installs get an optimized class map, APCu is suggested so the existing autoloader cache setting can engage, and plugin autoloaders no longer sit in front of the core one where every core class lookup had to pass through them first.\n    * Rendering a page with cached content no longer loads its whole media collection up front, the pages index no longer stores pre-computed metadata for every page, and relative markdown links resolve their target page directly instead of building the full page list per link.\n    * Assorted hot-path trims: string helpers use fast native functions for the common case, asset rendering skips per-asset integrity work when the feature is off, the site root URL is computed once per request instead of per asset, and debugger timers cost nothing when the debugger is disabled.\n    * New experimental opt-in page index (`pages.lazy_index: true`): pages, routes, children lists, sort orders and the taxonomy map load on demand from a per-page index instead of one large cache blob that has to be fully unserialized on every request, making per-request cost flat as sites grow: a 2,000 page test site renders as fast as a 2 page one and uses a quarter of the memory; SQLite powers the index when available with a pure PHP fallback, and the default behavior is completely unchanged until the flag is enabled.\n    * Page collection filters (`visible`, `routable`, `published`, module) now use menu flags recorded in the page index, so building a navigation menu that filters a folder to its visible pages no longer loads every hidden sibling first. On a 500 post blog under the Quark theme this cut the pages built for a page view from all 507 to 7 and roughly halved memory; it helps every site, most of all large ones with the experimental page index enabled.\n    * Sorting a page collection by date, title, or another common field now reads that value from the page index instead of loading every page in the collection just to read one field, and on single language sites the automatic translated filter that every collection applies no longer loads any pages at all. With the experimental page index enabled, a blog post showing a related posts grid dropped from loading every post on the site to only the handful it displays.\n    * The setting that scanned rendered page content for XSS is retired, as the gap it covered is now closed when the page is saved rather than every time it is viewed; the old toggle is removed from your security configuration automatically on upgrade.\n"},"2.0.6":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * [security] Flex user avatars stored under `user\/accounts\/<username>\/` (folder storage) are now served too; the 2.0.5 avatar carve-out only covered the flatfile `user\/accounts\/avatars\/` layout, so folder-storage avatars kept returning a 403. Existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"2.0.5":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * A page's `translatedLanguages()` now returns each language's own route, so a translation with a localized `slug:` produces the correct cross-language link instead of repeating the default language's URL. Fixes [getgrav\/grav#4183](https:\/\/github.com\/getgrav\/grav\/issues\/4183).\n    * [security] Profile avatars display again instead of returning a 403; the folder hardening that locked down `user\/accounts` now makes a narrow exception for avatar images while account data such as password hashes stays blocked, and existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n    * Loading a page no longer fails with a \"Failed to write cache file\" error when Grav can't save the compiled template cache, such as on a shared folder, a full disk, or during a save-then-reload race; the page still renders and the problem is logged instead. Fixes [getgrav\/grav#4184](https:\/\/github.com\/getgrav\/grav\/issues\/4184).\n"}},"families":{"stable":{"2.0":{"version":"2.0.24","date":"2026-09-03T20:39:28Z","url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/2.0.24","min_php":"7.2.3","assets":{"grav-admin":{"name":"grav-admin-v2.0.24.zip","type":"binary\/octet-stream","size":23112034,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/2.0.24","content_sha256":"d470152707ad2e8d353b4cc8f7acecbc2ca286a02e20914ac2731af98d50c170","sha256":"8f44dda40dac243c2ea538e85040f139fbebb58dd812f138c9e205fd779e7042"},"grav-update":{"name":"grav-update-v2.0.24.zip","type":"binary\/octet-stream","size":8349754,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/2.0.24","content_sha256":"a190a0b047099d023411e01c2e5a3240a153325f76a2d0adaae46589ac6eec4f","sha256":"cf2f3761ff039a45415c260338d9309ed1427c2436bdec48f74a984e0a84038e"},"grav":{"name":"grav-v2.0.24.zip","type":"binary\/octet-stream","size":10209678,"download":"https:\/\/getgrav.org\/download\/core\/grav\/2.0.24","content_sha256":"7cc47cce425c8e0a0fc108a003c6d9a38d95341491250d53ae7005fa1dacc1a8","sha256":"d1cb3c52e12d8f3b3e35c94c2c6386a17554264161e59fb57176ef1698743c57"}},"changelog":{"2.0.24":{"date":"09\/03\/2026","content":"1. [](#new)\n    * **A dependency can now name the generation of Grav it is for.** A plugin that supports both 1.7 and 2.0 often needs a different version of the same dependency on each, so a `dependencies` entry takes an optional `grav` key: `- { name: form, version: '>=9.1.0', grav: '2.0' }`. Entries without it apply everywhere, so existing blueprints are unchanged. See [Plugin Compatibility](https:\/\/learn.getgrav.org\/20\/plugins\/plugin-compatibility#requiring-different-versions-per-grav-generation)\n    * A new `system.images.progressive_jpeg` setting, on by default, controls whether resized and cached JPEGs are saved as progressive\n\n1. [](#bugfix)\n    * Installing a package whose dependency is not in the GPM index now says so and carries on, instead of stopping the command with a PHP fatal error. A plugin that still asks for the Grav 1.7 admin plugin was enough to trigger it [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * A plugin that asks for the `admin` plugin now has that read as Admin 2 on Grav 2, so a plugin written for both 1.7 and 2.0 installs instead of failing on a dependency that cannot exist there. The version it asks for is not carried over, because it describes the old admin's numbering [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * Any other dependency that cannot be installed on this generation of Grav is now left out of the install rather than attempted and failed\n    * A JSON request whose body is a bare scalar (`\"text\"`, `12345`, `true`) sent with `Content-Type: application\/json` no longer answers a 500 from the request pipeline before any route runs. It is treated as an empty body, so a plugin's webhook or API route gets to answer it, log it and refuse it itself. With `errors.display` on, the old failure also printed a stack trace with server paths to whoever sent it\n    * An image default such as `resize` set in `system.images.defaults` works again. Every image manipulation was being skipped since 2.0.22, leaving only the loading and decoding hints [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Settings such as `loading: lazy` now stay on the image when `link` is also on, instead of moving onto the surrounding link where the browser never sees them [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Resized and cached JPEGs are saved as progressive again, so a photo appears as a whole blurry image that sharpens instead of filling in one line at a time. It has been Grav's default since 2014 but silently stopped working in 1.4.6 [#4284](https:\/\/github.com\/getgrav\/grav\/issues\/4284)\n    * A media file's own settings from `media.yaml` are no longer overwritten the moment the image is opened, so custom default filters for an image type work again\n    * A URL typed with a trailing slash no longer errors out on a site that has the debugger switched on and the Login plugin protecting page media, because the debug bar is now skipped on redirects, where there is no page to put it on [#4280](https:\/\/github.com\/getgrav\/grav\/issues\/4280)\n    * With the optional `system.session.read_and_close` setting turned on, a change made to the session early in a request is no longer thrown away by a later write in that same request, and a message added just before a redirect now reaches the page it was meant for [#4281](https:\/\/github.com\/getgrav\/grav\/issues\/4281)\n"},"2.0.23":{"date":"09\/02\/2026","content":"1. [](#improved)\n    * `composer.json` now declares the `ctype` and `session` extensions it has always used, and suggests `fileinfo` and `simplexml`. Installing with `composer create-project` on a machine missing one of these no longer quietly walks back to a years-old release of Grav instead of failing [#4273](https:\/\/github.com\/getgrav\/grav\/discussions\/4273)\n1. [](#bugfix)\n    * Reading the browser name, platform or version no longer raises a PHP deprecation notice when a request arrives with no user agent, which is every request from a bare script or a health check\n    * Opening Clockwork before anything has been profiled, on a fresh install or right after `bin\/grav clear`, now reports that there is no data yet instead of failing with a 500\n    * A `GRAV_CONFIG__` override set to `true` or `false` now reaches the configuration as a real yes\/no value instead of the word itself, so switching something off from a `.env` file or the server environment actually switches it off. Thanks to @nerdyjan for the report and @AdilAzhariOmsan for the fix [#4277](https:\/\/github.com\/getgrav\/grav\/issues\/4277)\n    * `Uri::ip()` now reads the visitor's address from `$_SERVER`, falling back to the environment, so hosts that don't hand request variables to PHP's environment no longer report every visitor as `UNKNOWN`. Anything that counts per address there, such as the Login plugin's failed-login lockout and per-IP rate limiting, had been sharing a single bucket. Thanks to @sandymac [#2507](https:\/\/github.com\/getgrav\/grav\/issues\/2507)\n    * On those same hosts the `system.http_x_forwarded` options for `ip`, `client_ip` and `cf_connecting_ip` had no effect at all, and now work as documented. If you turned one on and saw nothing change, turn it back off unless the site really is behind a proxy that overwrites that header\n"},"2.0.22":{"date":"08\/31\/2026","content":"1. [](#new)\n    * The `url()` Twig function now takes a language, so a link to a route that isn't a page - a search page, a form action - can carry the site's language prefix: `{{ url('\/search', lang=true) }}`\n    * Blueprints can use a `media` field type, which saves a picked file as its path and keeps a list of them when the field allows more than one\n    * Page collections can now exclude one or more template types with `notOfType()`, the counterpart to the existing `ofType()` [#3910](https:\/\/github.com\/getgrav\/grav\/issues\/3910)\n    * The scheduler can now run the jobs that have missed their scheduled time, rather than only the ones due this very minute. Run it with `bin\/grav scheduler --catch-up`, which is what you want on a site that has no cron entry set up\n1. [](#improved)\n    * Twig in page content can no longer read the site's configuration through the `print_r`, `vardump`, `json_encode`, `yaml_encode` and `string` filters. The check that was meant to stop it had been asking whether the whole site was in sandbox mode, which Grav never does \u2014 it decides per template \u2014 so it had been letting everything through. Thanks to @Vectrain51\n    * The `|map`, `|filter` and `|reduce` Twig filters now check for themselves whether the template calling them is sandboxed, rather than relying on Twig to work it out. Thanks to @DhiyaneshGeek\n    * The scheduler no longer rebuilds a queued job from an unsigned queue file, so a file written into the queue folder by something other than Grav can at most re-run a job the site was already set up to run. The page index also refuses to build objects while reading its cache. Thanks to @elite0529\n    * Plugin and theme descriptions are now rendered in Parsedown's safe mode before admin displays them, so a description carrying raw HTML shows as text rather than being rendered. Thanks to @alham-rizvi\n    * The debugger's Clockwork data endpoint now answers only requests coming from the server itself, or requests presenting the secret set in the new `debugger.token` option. Cookies and API tokens are no longer recorded in profiler data whatever the `censored` option is set to\n    * Building a URL is now faster, which adds up over the hundreds of asset and link URLs a single page render produces\n    * The scheduler now records whether a run was started by cron or by hand, and a run you started yourself no longer counts as evidence that cron is set up\n    * `bin\/grav scheduler -r` now records the run against each job, the same as a scheduled run, so the next run knows what has already happened\n    * A field that is rejected only for being too long or too short now says so, and gives both the length submitted and the limit, instead of the same \"Invalid input\" any other bad value gets\n    * Multiline fields no longer carry a length limit low enough to affect real writing. Set `max: 0` on a field to remove the limit altogether\n1. [](#bugfix)\n    * The `|reduce` Twig filter now actually reduces. It was running the `|map` code by mistake and throwing away the starting value, so `[1,2,3]|reduce((c, v) => c + v, 0)` gave back a list instead of `6`. Thanks to @DhiyaneshGeek\n    * A form field's `minlength` and `maxlength` are now checked when the form is submitted, not only by the browser. They were being written into the page as HTML attributes but ignored on the server, so anything that skipped the browser's own check went straight through [#642](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/642)\n    * A field with a `step` set now accepts the lengths and counts that land on a step, and rejects the ones that do not. The check was the wrong way round, so it rejected exactly the values it was meant to allow\n    * A long page can be saved from the admin again. Page content was capped at 65,536 characters, so anything longer than roughly twenty pages of text was refused, and the only way to edit it was to write the file directly [#3643](https:\/\/github.com\/getgrav\/grav\/issues\/3643)\n    * A site installed in a subfolder no longer mangles URLs whose path repeats the install folder's name, such as an image at `\/images\/subdir\/photo.jpg` on a site installed at `\/subdir`\n    * A link to a page that carries a query string or an anchor, such as `\/blog?page=2`, now resolves to the page and keeps its language prefix, instead of being passed through as a plain path\n    * On a site installed in a subfolder, links written with the full path now resolve to the page, so they pick up the site's language and page extension\n    * A cache folder that the web server cannot write to no longer takes the whole site down. Grav now logs a warning naming the folder and serves the request without the cache, so the front end and the admin both stay reachable and the Problems plugin can report what is wrong [#4260](https:\/\/github.com\/getgrav\/grav\/issues\/4260)\n    * The same failure writing `user\/config\/versions.yaml` no longer stops the site either [#3688](https:\/\/github.com\/getgrav\/grav\/issues\/3688)\n    * Errors about a file that cannot be written now name the folder and say whether it is missing or not writable, instead of only reporting the file\n    * A session cookie name starting with `__Secure-` or `__Host-` now keeps that prefix and is sent with the settings browsers require for it, so the extra protection those prefixes give actually applies. Thanks to @wakqasahmed for the fix [#3773](https:\/\/github.com\/getgrav\/grav\/issues\/3773)\n      Note: sites whose `system.session.name` contains capitals, underscores or a leading or trailing dash will get a slightly different cookie name after this update, which signs their users out once.\n    * The scheduler no longer reports that cron is not set up when the crontab entry is written in a valid but slightly different style, such as one using `&&` or an absolute path to `bin\/grav`\n    * On a site with a custom scheduler job of its own, looking up a job by name no longer misses every job the system and its plugins register, so the backup and cache jobs can be found and run individually\n    * A scheduler job that finished its work but could not then write its output file, send its notification email or run its callback no longer aborts the whole run. The remaining jobs run, every result is still recorded, and the problem is written to the log\n    * A scheduler job registered without a schedule of its own no longer causes an error when its next run time is worked out\n    * `bin\/grav scheduler -j` no longer fails on a site with jobs registered by a plugin, and `-d` no longer fails on a job that has never run\n    * A scheduler job that runs one of Grav's own command line scripts now works when the scheduler is triggered from the web rather than from cron. Those jobs used to fail with \"env: php: No such file or directory\", because the web server does not have php on its path\n    * A scheduler job registered as a whole command line, such as `bin\/plugin myplugin sync`, now runs. Only the executable and its arguments given separately used to work, so a job written the other way looked for a file whose name contained spaces and failed every time it ran\n    * A site served from a subpath by a proxy no longer loses that subpath when a trailing slash is redirected, which previously sent visitors outside the site. The homepage of such a site also no longer redirects to the bare domain. Thanks to @wakqasahmed for the fix [#3822](https:\/\/github.com\/getgrav\/grav\/issues\/3822)\n    * With `force_ssl` turned on, a page that does not exist now redirects to HTTPS like every other page, instead of serving the 404 over plain HTTP. Thanks to @wakqasahmed for the fix [#3703](https:\/\/github.com\/getgrav\/grav\/issues\/3703)\n    * Image settings are no longer applied to audio, video, SVG or document media. An embedded MP3 kept its player instead of being turned into a linked thumbnail, and media URLs no longer pick up stray `loading`, `decoding` and `fetchpriority` values, which happened on every site whether or not those settings had been changed. Thanks to @wakqasahmed for the fix [#4264](https:\/\/github.com\/getgrav\/grav\/issues\/4264)\n"},"2.0.21":{"date":"08\/22\/2026","content":"1. [](#bugfix)\n    * Form fields no longer print their HTML attributes as text above the field, a problem the Twig update in 2.0.20 introduced on every form [#4256](https:\/\/github.com\/getgrav\/grav\/issues\/4256)\n    * A custom text escaper registered by a plugin now works again, instead of stopping the page with an error the first time a template used it\n"},"2.0.20":{"date":"08\/21\/2026","content":"1. [](#improved)\n    * Updated the bundled Twig fork to the current 3.x, picking up the correctness and sandbox improvements from the 3.27 and 3.28 releases.\n    * Grav now runs on Twig 3.28 and newer, which tightened the escaping method that Grav's compatibility shim replaces and would otherwise stop the site with a server error.\n    * The Twig content sandbox now accepts the list of tests a template uses, which newer Twig versions hand to it and will require from Twig 4.\n    * The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.\n    * Script and style files whose name already contains a version, such as those the Admin panel ships, are cached permanently in that same configuration, because a change always produces a new name.\n1. [](#bugfix)\n    * [security] Page content can no longer register a script or stylesheet through the Twig content sandbox, and asset URLs are now escaped where the tag is built, closing a way to inject markup into a rendered page.\n    * [security] The `read_file` capability no longer includes the user data folder by default, so page content can no longer be used to publish form submissions and other stored data.\n    * [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.\n    * [security] Custom Twig sandbox denial rules now take effect regardless of how the class name is capitalised, and can no longer be silently bypassed through a parent class or interface.\n    * A damaged page cache file is now rebuilt from the original page instead of stopping the site with a server error [#4239](https:\/\/github.com\/getgrav\/grav\/issues\/4239)\n    * Images and links in page content now work when the file name contains a colon, such as a screenshot named after a timestamp [#3933](https:\/\/github.com\/getgrav\/grav\/issues\/3933)\n    * A page that sets a full web address as its canonical route now uses that address on its own, instead of joining it onto the site's own address and breaking sitemaps and canonical links [#4023](https:\/\/github.com\/getgrav\/grav\/issues\/4023)\n    * Turning on asset timestamps now gives each stylesheet and script its own marker taken from when that file last changed, so editing one file no longer waits on an unrelated change before visitors see it [#4049](https:\/\/github.com\/getgrav\/grav\/issues\/4049)\n    * A canonical route set through the Flex pages API is now saved as written, instead of being stored in a form it could never be read back from.\n    * Flex directory blueprints no longer lose the fields the Flex Objects plugin adds when something reads the directory early in a request [#160](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/160)\n    * The scheduler's generated cron command now names the site's environment when that environment has its own configuration, and each run records which environment it used, so custom jobs defined in `user\/env\/<host>\/` no longer fail silently from cron [#4248](https:\/\/github.com\/getgrav\/grav\/issues\/4248)\n    * Audio and video players generated by `media.html()` no longer carry an `alt` attribute, which isn't valid on those elements; any alternative text is kept as an accessible label instead, so the markup passes validation [#3540](https:\/\/github.com\/getgrav\/grav\/issues\/3540)\n"},"2.0.19":{"date":"08\/14\/2026","content":"1. [](#new)\n    * You can now tighten the Twig content sandbox below its built-in defaults with new `denied_*` settings in `security.yaml`.\n    * The \"Twig in Content\" report can show the effective sandbox policy, so you can see exactly what page content is allowed to do.\n1. [](#improved)\n    * A theme or plugin that ships its own `.htaccess` can no longer switch off the protection on its own folder, which used to leave its configuration and template files downloadable [#4236](https:\/\/github.com\/getgrav\/grav\/issues\/4236)\n    * Twig in page content now renders on new installs by default, instead of appearing as raw text until the setting was turned on.\n    * The long Twig sandbox allowlists now ship built into Grav, so `security.yaml` only records your own additions and future security updates to the defaults reach every site.\n    * Existing sites that had trimmed those allowlists to tighten them keep exactly that policy after upgrading, now recorded as explicit `denied_*` entries.\n    * Removed two rarely-used Twig sandbox switches (`logging` and `admin_hint`); both behaviours are now always on.\n1. [](#bugfix)\n    * The content cross-site scripting check no longer objects to harmless `<option>` and `<select>` markup, whose original issue is fixed in the form field that actually rendered it.\n    * Sites running with the Twig 2 compatibility setting no longer crash with a server error on every page once an update clears the template cache [#4235](https:\/\/github.com\/getgrav\/grav\/issues\/4235)\n"},"2.0.18":{"date":"08\/11\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.14, which closes two further ways a crafted stylesheet could hide an external image reference from the checks added in the previous release ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n    * [security] Modular pages are now checked for cross-site scripting when they are saved, closing a way for a page editor to store a script that ran for every visitor ([GHSA-fg8g-663r-f366](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fg8g-663r-f366)).\n    * [security] The Twig `sort` and `find` filters no longer run a plain function name as a callable inside the content sandbox, closing a way for a page editor to execute arbitrary PHP ([GHSA-p6qj-p5m7-f62h](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p6qj-p5m7-f62h)).\n    * A blueprint that builds on another one can again fill dropdowns from its own PHP, which mostly affected themes because their page blueprints nearly always extend the default one ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)).\n    * The Scheduler no longer fails outright on hosts that disable PHP's `proc_open`, so scheduled jobs can still be viewed and edited there [getgrav\/grav-admin-next#16](https:\/\/github.com\/getgrav\/grav-admin-next\/issues\/16)\n    * A scheduled job that cannot be started on such a host is now reported as failed with an explanation, instead of stopping the whole scheduler run\n    * Grav now works out who the site runs as without starting a shell, so that detail still appears when external commands are unavailable\n    * The record of when the scheduler last ran is now written to a fixed location rather than one relative to wherever the trigger happened to run from\n1. [](#improved)\n    * Grav now decides whether the scheduler is being triggered by checking that each job has run when its own schedule says it should have, instead of requiring a run in the last two minutes, so a sparse crontab, a webhook or a scheduled task on Windows all count\n"},"2.0.17":{"date":"08\/07\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.13, which stops CSS comments from hiding dangerous values and covers image loading through `image-set()`, so untrusted SVG or HTML can no longer reference external resources those ways ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n"},"2.0.16":{"date":"08\/07\/2026","content":"1. [](#improved)\n    * Updated vendor libs to latest versions\n    * The list of groups on the account form is now read through the current user groups system, retiring a routine deprecated since Grav 1.7.\n1. [](#bugfix)\n    * [security] The site, system and theme settings offered to Twig written inside page content are now filtered by the same denied-paths list that already covered `config`, so a page editor can no longer read secrets such as a Redis password straight out of them ([GHSA-p597-crqc-m349](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p597-crqc-m349)).\n    * [security] Twig written into a form's email settings now runs under the same restrictions as Twig written into page content, closing a route that let someone with only page-editing rights run commands on the server ([GHSA-gh8j-q67c-j53f](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-gh8j-q67c-j53f)).\n    * [security] Form security tokens are now compared with a routine that takes the same amount of time whichever characters differ, so the check can no longer hint at how much of a guess was right ([GHSA-38p6-h87p-r4cg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-38p6-h87p-r4cg)).\n    * [security] The check for whether a visitor arrived from your own site now requires a full address match, so another site whose domain merely begins with yours no longer counts as your own ([GHSA-9ccq-2jfg-qw33](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-9ccq-2jfg-qw33)).\n    * [security] Scheduler job locks are now kept inside your site rather than in the shared system temp folder, so another account on the same server can no longer redirect a lock write to a file of its choosing ([GHSA-q8w8-6cq5-j4h2](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q8w8-6cq5-j4h2)).\n    * A scheduled job set to run only one at a time now refuses to start when its lock cannot be written, instead of running unprotected. Note that `bin\/grav clear` removes these locks along with the rest of the temporary folder.\n    * [security] Deleting, renaming and copying a media file now check the whole path rather than just the file's own name, so a plugin calling those routines directly cannot reach a file outside the media folder ([GHSA-jq29-c7v8-rg55](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-jq29-c7v8-rg55)).\n    * [security] The `media_directory()` Twig function now only accepts folders inside your site, so Twig written into page content can no longer list files or republish images from elsewhere on the server ([GHSA-47ch-6w46-6xm7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-47ch-6w46-6xm7)).\n    * Deleting a media file no longer also removes the retina copies and metadata belonging to a different file whose name merely ends with the same text, so deleting `banner.jpg` leaves `my-banner@2x.jpg` alone.\n    * A fallback page handed to `Uri::referrer()` is now returned as given, instead of being trimmed away to nothing whenever the visitor did not arrive from your site.\n    * `Pages::referrerRoute()` was comparing a full web address against a site path, so it never recognised a match and always reported that the visitor came from somewhere else.\n"},"2.0.15":{"date":"08\/03\/2026","content":"1. [](#bugfix)\n    * A plugin's blueprints can use the data providers that plugin ships again, instead of having every one of them refused by a check that only recognised the providers core itself registers ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)). Fields defined in page frontmatter are held to the stricter rule instead, which is where the risk actually was.\n    * [security] A configuration admin can no longer reach an unvetted built-in routine by writing a blueprint field's data provider as a plain function name, a spelling that was still being checked against a list of known-bad names rather than the list of approved ones ([GHSA-f8wv-xp27-6gq7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-f8wv-xp27-6gq7)).\n    * [security] The content security scan now reads an unpaired quote inside an unquoted attribute value the way a browser does, closing another way a page editor could hide a script from it ([GHSA-vfmf-q6x9-cw96](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-vfmf-q6x9-cw96)).\n    * [security] The media URL in an audio or video tag is now escaped, so a filename carrying markup can no longer add its own attributes to the player ([GHSA-6qw9-4vv5-jr97](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-6qw9-4vv5-jr97)).\n    * JSON responses no longer fail outright when the data contains invalid UTF-8. `json_encode()` returns `false` on malformed bytes, and the PSR-7 response body is type-hinted `string|resource|StreamInterface`, so that `false` came back out as an unhandled `TypeError` from inside the vendor stream rather than as a response. Affected `createJsonResponse()` and both JSON error responses in `ControllerResponseTrait`, where an exception message carrying a bad byte would take out the error handler itself, plus the Clockwork data endpoint in `Debugger`. Bad bytes are now substituted, and the remaining structural failures (recursion depth, `INF`\/`NAN`) raise a catchable `JsonException` instead of a silent `false`. Output for valid data is unchanged.\n    * [security] The fast static asset server now keeps a request inside the directory the site published, instead of also allowing any neighbouring directory whose name starts with the same letters ([GHSA-4v9q-p283-qc2m](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-4v9q-p283-qc2m)).\n    * [security] File uploads now reject a few more extensions that browsers run script from, or that a server may hand to PHP: `xhtml`, `xht`, `svgz`, `php7`, `php8`, `pht`, `phtm` and `phps` ([GHSA-66xf-ggf4-6hmc](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-66xf-ggf4-6hmc)).\n    * [security] The bundled `Caddyfile` protections did nothing. They were written as nginx-style regexes, which Caddy reads as literal paths that never match, and the `respond` they redirected to ran after the catch-all rewrite had already claimed the request. A site served with this config handed out `user\/accounts\/`, `user\/config\/`, `logs\/`, `composer.lock`, page files, and the `system\/` and `vendor\/` folders to anyone who asked. The rules are now named `path_regexp` matchers answering `403` directly, inside a `route` block so they run before the rewrite, and they were checked request by request against the `.htaccess` behaviour.\n"},"2.0.14":{"date":"07\/30\/2026","content":"1. [](#improved)\n    * Removed old JQuery `2.1.4`. However `2.2.4` and `3.7.1` remain for legacy support\n1. [](#bugfix)\n    * [security] An administrator with account-management rights can no longer grant themselves super-admin access by saving it into a user group's permissions, a protection the account form already had ([GHSA-xhfv-7758-r9hx](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xhfv-7758-r9hx)).\n    * [security] The content security scan no longer reports a page as clean when it was unable to examine it, closing two ways a page editor could hide a script from it: a single invalid character anywhere in the content, and padding a tag out to several thousand characters ([GHSA-q2j8-x8hf-63ch](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q2j8-x8hf-63ch)).\n"},"2.0.13":{"date":"07\/25\/2026","content":"1. [](#new)\n    * Added an `array_group_by` Twig filter and function for grouping a list of items by one of their values ([#4218](https:\/\/github.com\/getgrav\/grav\/pull\/4218)).\n1. [](#bugfix)\n    * [security] A configuration admin can no longer run code on the server by pointing a dynamic field's data provider at a built-in routine named as a class-and-method pair, a form that slipped past the safety check because it only inspected the single-string spelling; both forms are now vetted the same way ([GHSA-r94f-hx44-8jqf](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-r94f-hx44-8jqf)).\n    * [security] A page editor without super-admin rights can no longer store an event-handler script that runs for site visitors by hiding it behind a `>` placed inside a quoted HTML attribute; the content security scan now reads quoted attribute values the same way a browser does ([GHSA-269c-h76q-8cxw](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-269c-h76q-8cxw)).\n    * [security] A backup profile's location is now confined to the site folder, so a profile pointing outside the Grav root can no longer pull external directories into the backup archive ([GHSA-fch7-cpv4-w7hg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fch7-cpv4-w7hg)).\n    * [security] Uploaded filenames may no longer contain the HTML characters `<`, `>`, or `\"`, so a stored filename cannot carry markup that could run if it were later shown unescaped.\n    * [security] The `find` and `sort` Twig filters now reject a dangerous function name given as their callback, matching the protection already applied to `map`, `filter`, and `reduce`, so template values cannot use them to run code ([GHSA-xx48-97m4-h7qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xx48-97m4-h7qm)).\n    * The bundled `nginx.conf` security rules are now anchored to the start of the path like the `.htaccess` rules already are, so the admin's Tools \u2192 Logs viewer works on nginx instead of being blocked ([#4223](https:\/\/github.com\/getgrav\/grav\/pull\/4223)).\n    * On non-FastCGI setups Grav no longer sends an invalid `Content-Encoding: none` header, which some strict HTTP clients rejected outright; it now closes the connection cleanly without the bogus value ([#2619](https:\/\/github.com\/getgrav\/grav\/issues\/2619)).\n"},"2.0.12":{"date":"07\/20\/2026","content":"1. [](#new)\n    * Added per-language fallbacks for unsupported browser languages during `HTTP_ACCEPT_LANGUAGE` negotiation, allowing them to resolve to supported languages without exposing additional language routes.\n1. [](#improved)\n    * The `read_file()` Twig function can now read `.css` files by default, so inline stylesheets can be embedded in a template without adjusting the security config ([#4215](https:\/\/github.com\/getgrav\/grav\/issues\/4215)).\n    * Documented that the site-wide media object resolves lazily, so its inherited collection query methods need `media_directory()` to filter or sort site media ([#4210](https:\/\/github.com\/getgrav\/grav\/issues\/4210)).\n1. [](#bugfix)\n    * Browser language codes are now matched case-insensitively during `HTTP_ACCEPT_LANGUAGE` fallback negotiation.\n"},"2.0.11":{"date":"07\/13\/2026","content":"1. [](#new)\n    * You can now filter, sort, and group a page's media by the values in their `.meta.yaml` metafiles directly in Twig, with new `filterBy`, `where`, `findBy`, `sortBy`, `groupBy`, and `withMeta` methods on `page.media`. Fixes [getgrav\/grav#4200](https:\/\/github.com\/getgrav\/grav\/issues\/4200).\n1. [](#bugfix)\n    * [security] A page editor can no longer read arbitrary files from the server by pointing an image watermark at a traversal path such as `carrier.png?watermark=..\/secret.png`; an editor-supplied watermark path is now confined to the site's media, while operator-configured watermarks and stream URIs are unaffected ([GHSA-w3f4-8pj2-599w](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-w3f4-8pj2-599w)).\n    * [security] A page-edit account can no longer reach file-disclosure or secret-read functions by naming an arbitrary `Class::method` as a dynamic field's data provider; qualified providers are now limited to a known-safe allowlist, closing a bypass of the guard added in 2.0.7 and 2.0.9 ([GHSA-7pgq-cr25-xvc8](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-7pgq-cr25-xvc8), [GHSA-cxv3-5jj3-cpgr](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-cxv3-5jj3-cpgr)).\n    * A page is no longer blanked when viewed just because a trusted plugin or shortcode on it outputs markup the content security scan flags, such as an embed, form, or icon; the check that guards against dangerous editor content now runs once when the page is saved rather than every time it is rendered ([GHSA-2c4f-86xc-cr74](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-2c4f-86xc-cr74)).\n1. [](#improved)\n    * [security] Page content that uses Twig to assemble disallowed markup at render time, such as building an event handler or a `<script>` tag from separate pieces, is now refused when you save the page instead of being allowed through to visitors.\n    * The `raw` Twig filter is no longer allowed inside editor-authored page content, so page content can no longer output unescaped dynamic values past the content security check; trusted theme templates are unaffected.\n"},"2.0.10":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * A partial `system.pages.process` override in `user\/config\/system.yaml` (for example setting only `twig: false`) no longer silently turns off Markdown and leaves every page rendering its raw source. Because `pages.process` is a single field in the blueprint, a partial override replaced the whole map and dropped the default `markdown: true`; core now re-applies that default, so an affected site recovers on update with no change to its config. An explicit `markdown: false` is still honored.\n"},"2.0.9":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * [security] A callable directive hidden in a Flex directory's blueprint field can no longer run commands on the server; dynamic field data on Flex types (Flex Pages, Flex Users, Flex Objects) now refuses dangerous functions, closing a path that bypassed the same guard added for page forms in 2.0.7 ([GHSA-c4wf-2xxc-68qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-c4wf-2xxc-68qm)).\n    * [security] The scheduler webhook trigger now fails closed when no token is configured, so an enabled webhook left without a token can no longer run scheduled jobs for anonymous callers ([GHSA-xwv3-2mv2-w33x](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xwv3-2mv2-w33x)).\n    * Images and links whose filename contains spaces now render when the path is wrapped in angle brackets, for example `![](<My image.jpg>)`. Fixes [getgrav\/grav#4197](https:\/\/github.com\/getgrav\/grav\/issues\/4197).\n"},"2.0.8":{"date":"07\/06\/2026","content":"1. [](#bugfix)\n    * An email or `www.` URL used as the visible text of a Markdown link is no longer turned into a second, nested link when GFM autolinks are enabled. Fixes [getgrav\/grav#4191](https:\/\/github.com\/getgrav\/grav\/issues\/4191).\n    * `md5()` can once again be called as a Twig function, not just as the `|md5` filter, so themes and plugins that generate an id or cache-busting hash with `md5(...)` keep working instead of failing with an \"Unknown function\" error. Fixes [getgrav\/grav-theme-quark2#12](https:\/\/github.com\/getgrav\/grav-theme-quark2\/issues\/12).\n    * Replacing an image in place, such as swapping the image in a Flex object field, now shows the new image on the site instead of the previously cached version (updated `getgrav\/image` to v4.1.3, which includes the source file's modification time and size in the derivative cache key). Fixes [getgrav\/grav#4195](https:\/\/github.com\/getgrav\/grav\/issues\/4195).\n"},"2.0.7":{"date":"07\/04\/2026","content":"1. [](#bugfix)\n    * [security] A page editor can no longer run commands on the server by hiding a callable directive in a form field's settings; dynamic field data now refuses dangerous functions and cannot be tricked into reaching one through a helper ([GHSA-fj2p-qj2f-74v5](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fj2p-qj2f-74v5)).\n    * A page's `translatedLanguages()` now localizes ancestor slugs too, so a nested translation whose parent folder has a localized `slug:` produces the fully translated cross-language link instead of leaving parent segments in the current language. Fixes [getgrav\/grav#4186](https:\/\/github.com\/getgrav\/grav\/issues\/4186).\n    * Pointing the log stream at `environment:\/\/` (for example `log: environment:\/\/logs`) no longer crashes the site or `bin\/grav clear` with a \"stream must either be a resource or a string\" error when the per-environment folder does not exist; logging now falls back to the default `logs\/` folder instead. Fixes [getgrav\/grav#4172](https:\/\/github.com\/getgrav\/grav\/issues\/4172).\n    * The `media:\/\/` stream now checks the per-environment `user\/env\/<host>\/media\/` folder before the shared `user\/media\/`, so site media stored per environment resolves to the correct URL in the admin and in page content instead of a broken `user\/media\/` link. Fixes [getgrav\/grav#4188](https:\/\/github.com\/getgrav\/grav\/issues\/4188).\n    * Large file downloads such as site backups are now streamed to the browser in chunks instead of being loaded into memory all at once, so a download bigger than PHP's memory limit no longer fails with a blank server error. Fixes [getgrav\/grav-plugin-api#12](https:\/\/github.com\/getgrav\/grav-plugin-api\/issues\/12).\n    * Backups initialization no longer runs twice when something that bypasses the normal request middleware (such as the API plugin) also attaches the backup scheduler listener, so the listener is registered only once.\n    * Pages accessed with URL parameters such as pagination or taxonomy filters no longer recompile every Twig template on each request, restoring full template caching on exactly the pages that get the most traffic.\n    * The per-file compiled cache for YAML and markdown files now loads through its intended opcache fast path, and a source file that has been deleted no longer serves stale cached data.\n    * A modular page that outputs trusted theme or plugin markup, such as a form with a reCAPTCHA field, is no longer wrongly blanked by the content security scan, which now checks the editor's own content instead of the finished template output. Fixes [getgrav\/grav-plugin-form#636](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/636).\n    * Chaining media actions on page media under the content security scan, such as `{{ page.media['x.jpg'].lightbox(1024,768).cropResize(176,176).html() }}`, now works instead of being blocked, and the scan's list of allowed media methods stays in step with Grav's documented media actions automatically.\n1. [](#improved)\n    * Updating a plugin or theme whose required dependency is held back by a newer Grav or PHP requirement now explains the real fix. Instead of reporting that the needed version is \"higher than the latest release\" and suggesting a cache refresh, the updater names the newer dependency release and the Grav (or PHP) version it needs, so you know to update Grav first. Relates to [getgrav\/grav-plugin-admin2#93](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/93).\n    * Backup profiles now always appear in the scheduler where each can be switched on or off with the Enabled\/Disabled toggle, instead of a profile staying hidden until its schedule was turned on; the profile's schedule setting seeds the default state and an explicit toggle takes precedence.\n    * Frontend requests are noticeably faster across the board: the scheduler, backups machinery, error page renderer and logger now initialize only when actually used instead of on every page view, cutting over 50 PHP files from a typical request.\n    * The filesystem scan that checks pages for changes now reuses its result for a couple of seconds (configurable with `cache.check.interval`), so busy sites no longer stat every page file on every single request; content edits still show up right away in normal editing workflows and admin saves remain instant.\n    * Configuration, blueprint and language file lists honor the same freshness window instead of checking every tracked file's timestamp three times per request, and theme blueprints no longer load at all on normal frontend page views.\n    * Class autoloading is faster: source installs get an optimized class map, APCu is suggested so the existing autoloader cache setting can engage, and plugin autoloaders no longer sit in front of the core one where every core class lookup had to pass through them first.\n    * Rendering a page with cached content no longer loads its whole media collection up front, the pages index no longer stores pre-computed metadata for every page, and relative markdown links resolve their target page directly instead of building the full page list per link.\n    * Assorted hot-path trims: string helpers use fast native functions for the common case, asset rendering skips per-asset integrity work when the feature is off, the site root URL is computed once per request instead of per asset, and debugger timers cost nothing when the debugger is disabled.\n    * New experimental opt-in page index (`pages.lazy_index: true`): pages, routes, children lists, sort orders and the taxonomy map load on demand from a per-page index instead of one large cache blob that has to be fully unserialized on every request, making per-request cost flat as sites grow: a 2,000 page test site renders as fast as a 2 page one and uses a quarter of the memory; SQLite powers the index when available with a pure PHP fallback, and the default behavior is completely unchanged until the flag is enabled.\n    * Page collection filters (`visible`, `routable`, `published`, module) now use menu flags recorded in the page index, so building a navigation menu that filters a folder to its visible pages no longer loads every hidden sibling first. On a 500 post blog under the Quark theme this cut the pages built for a page view from all 507 to 7 and roughly halved memory; it helps every site, most of all large ones with the experimental page index enabled.\n    * Sorting a page collection by date, title, or another common field now reads that value from the page index instead of loading every page in the collection just to read one field, and on single language sites the automatic translated filter that every collection applies no longer loads any pages at all. With the experimental page index enabled, a blog post showing a related posts grid dropped from loading every post on the site to only the handful it displays.\n    * The setting that scanned rendered page content for XSS is retired, as the gap it covered is now closed when the page is saved rather than every time it is viewed; the old toggle is removed from your security configuration automatically on upgrade.\n"},"2.0.6":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * [security] Flex user avatars stored under `user\/accounts\/<username>\/` (folder storage) are now served too; the 2.0.5 avatar carve-out only covered the flatfile `user\/accounts\/avatars\/` layout, so folder-storage avatars kept returning a 403. Existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"2.0.5":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * A page's `translatedLanguages()` now returns each language's own route, so a translation with a localized `slug:` produces the correct cross-language link instead of repeating the default language's URL. Fixes [getgrav\/grav#4183](https:\/\/github.com\/getgrav\/grav\/issues\/4183).\n    * [security] Profile avatars display again instead of returning a 403; the folder hardening that locked down `user\/accounts` now makes a narrow exception for avatar images while account data such as password hashes stays blocked, and existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n    * Loading a page no longer fails with a \"Failed to write cache file\" error when Grav can't save the compiled template cache, such as on a shared folder, a full disk, or during a save-then-reload race; the page still renders and the problem is logged instead. Fixes [getgrav\/grav#4184](https:\/\/github.com\/getgrav\/grav\/issues\/4184).\n"}}},"1.7":{"version":"1.7.53.3","date":"2026-08-31T03:52:16Z","url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/1.7.53.3","min_php":"7.2.3","assets":{"grav-admin":{"name":"grav-admin-v1.7.53.3.zip","type":"binary\/octet-stream","size":19290359,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/1.7.53.3","content_sha256":"db740daf94780c29f9714759590557c657656ee1f44ed2489518804132c6c485","sha256":"18d249a4a8517b456d6abafb550488f966324d7ef171d08d0a338068525bd39f"},"grav-update":{"name":"grav-update-v1.7.53.3.zip","type":"binary\/octet-stream","size":9824469,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/1.7.53.3","content_sha256":"bb5b53737439feaa8d6d94b780e681fb46ddf79233fdfa03cacd63deda2d4a42","sha256":"3e53bde6e3c971151f13520306a2751106a948ce93bc77e0fc0e411302595318"},"grav":{"name":"grav-v1.7.53.3.zip","type":"binary\/octet-stream","size":12258277,"download":"https:\/\/getgrav.org\/download\/core\/grav\/1.7.53.3","content_sha256":"87f9b1078706e58f4135abcee8fbb1f0e9add9e05e5cab6f8a920c2924acc411","sha256":"11dfedf39caa01a4e69f4a0f333ac61c5b89936bf1b270de0161530f3084fe86"}},"changelog":{"1.7.53.3":{"date":"08\/31\/2026","content":"1. [](#improved)\n    * The debugger's Clockwork data endpoint now answers only requests coming from the server itself, or requests presenting the secret set in the new `debugger.token` option. Cookies and API tokens are no longer recorded in profiler data whatever the `censored` option is set to\n    * A field that is rejected only for being too long or too short now says so, and gives both the length submitted and the limit, instead of the same \"Invalid input\" any other bad value gets\n    * Multiline fields no longer carry a length limit low enough to affect real writing. Set `max: 0` on a field to remove the limit altogether\n1. [](#bugfix)\n    * [security] Form security tokens are now compared with a routine that takes the same amount of time whichever characters differ, so the check can no longer hint at how much of a guess was right ([GHSA-38p6-h87p-r4cg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-38p6-h87p-r4cg)).\n    * A long page can be saved from the admin again. Page content was capped at 65,536 characters, so anything longer than roughly twenty pages of text was refused, and the only way to edit it was to write the file directly [#3643](https:\/\/github.com\/getgrav\/grav\/issues\/3643)\n"},"1.7.53.2":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * [security] Flex user avatars stored under `user\/accounts\/<username>\/` (folder storage) are now served too; the 1.7.53.1 avatar carve-out only covered the flatfile `user\/accounts\/avatars\/` layout, so folder-storage avatars kept returning a 403. Existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"1.7.53.1":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * The `user\/data` block added in 1.7.53 now makes an exception for public media uploads, such as Flex Object images, so they keep displaying instead of returning a 403, while data files, databases and keys stay blocked. Fixes [getgrav\/grav#4129](https:\/\/github.com\/getgrav\/grav\/issues\/4129).\n    * [security] Profile avatars display again instead of returning a 403; the folder hardening that locked down `user\/accounts` now makes a narrow exception for avatar images while account data such as password hashes stays blocked, and existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"1.7.53":{"date":"06\/16\/2026","content":"1. [](#bugfix)\n    * [security] Direct web access to the `user\/accounts`, `user\/config`, `user\/data` and `user\/env` folders is now blocked outright in every bundled webserver config, closing a hole where files such as certificates, tokens and databases stored under `user\/data` with an unlisted extension could be downloaded directly.\n    * [security] A backup deny-all `.htaccess` now ships inside `user\/accounts`, `user\/config` and `user\/data` so Apache installs stay protected even when the site root `.htaccess` has been customised or is out of date.\n    * [security] The upgrade postflight now patches an existing stock root `.htaccess` to add the folder block automatically, so installs that updated from an earlier version are protected without editing the file by hand.\n    * [security] URL query image transforms (such as `image.jpg?resize=`) are now turned off by default and, when enabled, refuse oversized dimensions above a configurable pixel limit, closing an unauthenticated denial of service where huge resize values could exhaust server memory.\n"},"1.7.52":{"date":"04\/29\/2026","content":"1. [](#new)\n    * GPM client now sends the running PHP version with index requests so the server can substitute PHP-aware compat fallbacks when a plugin's latest release requires a newer PHP than the client can run.\n1. [](#bugfix)\n    * [security] Extended default `uploads_dangerous_extensions` to include `md`, `yaml`, `yml`, `json`, `twig`, `ini` \u2014 page-content extensions that can be weaponised via permissive form-upload `accept` policies (GHSA-w4rc-p66m-x6qq, defense-in-depth alongside the Form 9.1.0 plugin fix).\n"},"1.7.51":{"date":"04\/28\/2026","content":"1. [](#new)\n    * Added foundation for migrating to Grav 2.0: cross-major auto-upgrades are blocked in GPM, and core now surfaces a `next_major` hint so admin can point users at the new `migrate-grav` plugin\n    * Added `compatibility:` blueprint support so plugins\/themes can declare which Grav versions they support\n    * Added self-upgrade preflight that flags incompatible plugins\/themes and `psr\/log` \/ Monolog conflicts before proceeding\n    * Added upgrade resilience with automatic maintenance mode and opcache reset during self-upgrade\n    * Added new `cache-cleanup` CLI command to prune obsolete cache entries\n    * Added new `onFlexDirectoryConfigBeforeSave` event for Flex\n1. [](#improved)\n    * More readable time output in `bin\/grav logviewer` [#4009](https:\/\/github.com\/getgrav\/grav\/pull\/4009)\n    * Removed legacy standalone binary build\n    * Updated vendor libraries to latest versions\n1. [](#bugfix)\n    * Fixed `selectize` field losing values when keyed options were used\n    * Fixed wrong date output in `bin\/grav logviewer` [#4007](https:\/\/github.com\/getgrav\/grav\/pull\/4007)\n    * Fixed undefined array key error triggered by URL-encoded characters in paths [#4012](https:\/\/github.com\/getgrav\/grav\/pull\/4012)\n    * Fixed assorted issues in the revamped scheduler\n    * Fixed `schedule` flag not being honored in backup profiles\n    * Fixed default-language loading when using the session-based language store\n    * Allow `lang` query parameter to switch back to the default language\n"},"1.7.49.5":{"date":"09\/10\/2025","content":"1. [](#bugfix)\n    * Backup not honoring ignored paths [#3952](https:\/\/github.com\/getgrav\/grav\/issues\/3952)\n"},"1.7.49.4":{"date":"09\/03\/2025","content":"1. [](#bugfix)\n    * Fixed cron force running jobs severy minute! [#3951](https:\/\/github.com\/getgrav\/grav\/issues\/3951)\n"},"1.7.49.3":{"date":"09\/02\/2025","content":"1. [](#bugfix)\n    * Fixed an error in ZipArchive that was causing issues on some systems\n    * Fixed namespace change for `Cron\\Expression`\n    * Removed broken cron install field... use 'instructions' instead\n    * Fixed duplicate jobs listing in some CLI commands\n"},"1.7.49.2":{"date":"08\/28\/2025","content":"1. [](#bugfix)\n    * Fix translation of key for image adapter [#3944](https:\/\/github.com\/getgrav\/grav\/pull\/3944)\n"},"1.7.49.1":{"date":"08\/25\/2025","content":"1. [](#new)\n    * Rerelease to include all updated plugins\/theme etc.\n"},"1.7.49":{"date":"08\/25\/2025","content":"1. [](#new)\n    * Revamped Grav Scheduler to support webhook to call call scheduler + concurrent jobs + jobs queue + logging, and other improvements\n    * Revamped Grav Cache purge capabilities to only clear obsolete old cache items\n    * Added full imagick support in Grav Image library\n    * Added support for Validate `match` and `match_any` in forms\n1. [](#improved)\n    * Handle empty values on require with ignore fields in Forms\n    * Use `actions\/cache@v4` in github workflows\n    * Use `actions\/checkout@v4`in github workflows [#3867](https:\/\/github.com\/getgrav\/grav\/pull\/3867)\n    * Update code block in README.md [#3886](https:\/\/github.com\/getgrav\/grav\/pull\/3886)\n    * Updated vendor libs to latest\n1. [](#bugfix)\n    * Bug in `exif_read_data` [#3878](https:\/\/github.com\/getgrav\/grav\/pull\/3878)\n    * Fix parser error in URI: [#3894](https:\/\/github.com\/getgrav\/grav\/issues\/3894)\n\n"},"1.7.48":{"date":"10\/28\/2024","content":"1. [](#new)\n    * New Trait for fetchPriority attribute on images [#3850](https:\/\/github.com\/getgrav\/grav\/pull\/3850)\n1. [](#improved)\n    * Fix for #3164. Adds aliases as possible commands during lookup [#3863](https:\/\/github.com\/getgrav\/grav\/pull\/3863)\n1. [](#bugfix)\n    * Fix style conflict with Clockwork and tooltips [#3861](https:\/\/github.com\/getgrav\/grav\/pull\/3861)\n"},"1.7.47":{"date":"10\/23\/2024","content":"1. [](#new)\n  * New `Utils::toAscii()` method\n  * Added support for Clockwork Debugger to allow web UI (requires new `clockwork-web` plugin)\n1. [](#improved)\n  * Include modular sub-pages in last-modification date computation [#3562](https:\/\/github.com\/getgrav\/grav\/pull\/3562)\n  * Updated vendor libs to latest versions\n  * Updated JQuery to `3.7.1` [#3787](https:\/\/github.com\/getgrav\/grav\/pull\/3827)\n  * Updated vendor libraries to latest versions\n  * Support for Fediverse Creator meta tag [#3844](https:\/\/github.com\/getgrav\/grav\/pull\/3844)\n1. [](#bugfix)\n  * Fixes deprecated for return type in Filesystem with PHP 8.3.6 [#3831](https:\/\/github.com\/getgrav\/grav\/issues\/3831)\n  * Fix for `exif_imagtetype()` throwing an exception when file doesn't exist\n  * Fix JSON output comments check with content type [#3859](https:\/\/github.com\/getgrav\/grav\/pull\/3859)\n"},"1.7.46":{"date":"05\/15\/2024","content":"1. [](#improved)\n   * Better handling of external protocols in `Utils::url()` such as `mailto:`, `tel:`, etc.\n   * Handle `GRAV_ROOT` or `GRAV_WEBROOT` when `\/` [#3667](https:\/\/github.com\/getgrav\/grav\/pull\/3667)\n1. [](#bugfix)\n   * Fixes for multi-lang taxonomy when reinitializing the languages (e.g. LangSwitcher plugin)\n   * Ensure the full filepath is checked for invalid filename in `MediaUploadTrait::checkFileMetadata()`\n   * Fixed a bug in the `on_events` REGEX pattern of `Security::detectXss()` as it was not matching correctly.\n   * Fixed an issue where `read_file()` Twig function could be used nefariously in content [#GHSA-f8v5-jmfh-pr69](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-f8v5-jmfh-pr69)\n"},"1.7.45":{"date":"03\/18\/2024","content":"1. [](#new)\n   * Added new Image trait for `decoding` attribute [#3796](https:\/\/github.com\/getgrav\/grav\/pull\/3796)\n1. [](#bugfix)\n   * Fixed some multibyte issues in Inflector class [#732](https:\/\/github.com\/getgrav\/grav\/issues\/732)\n   * Fallback to page modified date if Page date provided is invalid and can't be parsed [getgrav\/grav-plugin-admin#2394](https:\/\/github.com\/getgrav\/grav-plugin-admin\/issues\/2394)\n   * Fixed a path traversal vulnerability with file uploads [#GHSA-m7hx-hw6h-mqmc](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-m7hx-hw6h-mqmc)\n   * Fixed a security issue with insecure Twig functions be processed [#GHSA-2m7x-c7px-hp58](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-2m7x-c7px-hp58) [#GHSA-r6vw-8v8r-pmp4](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-r6vw-8v8r-pmp4) [#GHSA-qfv4-q44r-g7rv](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-qfv4-q44r-g7rv) [#GHSA-c9gp-64c4-2rrh](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-c9gp-64c4-2rrh)\n1. [](#improved)\n   * Updated composer packages\n   * Updated `bin\/composer.phar` to latest `2.7.2`\n"},"1.7.44":{"date":"01\/05\/2024","content":"1. [](#new)\n   * Added PHP `8.3` to tests [#3782](https:\/\/github.com\/getgrav\/grav\/pull\/3782)\n   * Added debugger messages when Page routes conflict\n   * Added `ISO 8601` date format [#3721](https:\/\/github.com\/getgrav\/grav\/pull\/37210)\n   * Added support for `.vcf` (vCard) in media configuration [#3772](https:\/\/github.com\/getgrav\/grav\/pull\/3772)\n1. [](#improved)\n   * Update jQuery to `v3.6.4` [#3713](https:\/\/github.com\/getgrav\/grav\/pull\/3713)\n   * Updated vendor libraries including Dom-Sanitizer `v1.0.7` that addresses an XSS issue\n   * Updated `bin\/composer.phar` to latest `2.6.6`\n   * Updated vendor libraries to latest\n   * Updated language files\n   * Updated copyright year\n1. [](#bugfix)\n   * Fixed a math rounding issue with number validation when using floating point steps [#3761](https:\/\/github.com\/getgrav\/grav\/issues\/3761)\n   * Fixed an issue with `Inflector::ordinalize()` not working as expected [#3759](https:\/\/github.com\/getgrav\/grav\/pull\/3759)\n   * Fixed various issues with file extension checking with dangerous extensions [#3756(https:\/\/github.com\/getgrav\/grav\/pull\/3756)]\n   * Fix for invalid input to foreach in `UserGroupObject` [#3724](https:\/\/github.com\/getgrav\/grav\/pull\/3724)\n   * Fixed exception: `Property 'jsmodule_pipeline_include_externals' does not exist in object` [#3661](https:\/\/github.com\/getgrav\/grav\/pull\/3661)\n   * Fixed `too few arguments exception` in FlexObjects [#3658](https:\/\/github.com\/getgrav\/grav\/pull\/3658)\n"},"1.7.43":{"date":"10\/02\/2023","content":"1. [](#new)\n   * Add the ability to programatically set a page's `modified` timestamp via a `modified:` frontmatter entry\n2. [](#improved)\n   * Update vendor libraries\n   * Include `phar` in the list of `security.uploads_dangerous_extensions`\n   * When enabled `system.languages.debug` now dumps **Key -> Value** to debugger [#3752](https:\/\/github.com\/getgrav\/grav\/issues\/3752)\n   * Updated built-in composer to latest `2.6.4` [#3748](https:\/\/github.com\/getgrav\/grav\/issues\/3748)\n   * Added support for `@import` to ensure paths are rewritten correctly in CSS pipeline [#3750](https:\/\/github.com\/getgrav\/grav\/pull\/3750)\n"},"1.7.42.3":{"date":"07\/18\/2023","content":"2. [](#improved)\n   * Fixed a typo in `Utils::isDangerousFunction`\n"}}}},"testing":{"2.0":{"version":"2.0.24","date":"2026-09-03T20:39:28Z","url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/2.0.24","min_php":"7.2.3","assets":{"grav-admin":{"name":"grav-admin-v2.0.24.zip","type":"binary\/octet-stream","size":23112034,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/2.0.24?testing","content_sha256":"d470152707ad2e8d353b4cc8f7acecbc2ca286a02e20914ac2731af98d50c170","sha256":"8f44dda40dac243c2ea538e85040f139fbebb58dd812f138c9e205fd779e7042"},"grav-update":{"name":"grav-update-v2.0.24.zip","type":"binary\/octet-stream","size":8349754,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/2.0.24?testing","content_sha256":"a190a0b047099d023411e01c2e5a3240a153325f76a2d0adaae46589ac6eec4f","sha256":"cf2f3761ff039a45415c260338d9309ed1427c2436bdec48f74a984e0a84038e"},"grav":{"name":"grav-v2.0.24.zip","type":"binary\/octet-stream","size":10209678,"download":"https:\/\/getgrav.org\/download\/core\/grav\/2.0.24?testing","content_sha256":"7cc47cce425c8e0a0fc108a003c6d9a38d95341491250d53ae7005fa1dacc1a8","sha256":"d1cb3c52e12d8f3b3e35c94c2c6386a17554264161e59fb57176ef1698743c57"}},"changelog":{"2.0.24":{"date":"09\/03\/2026","content":"1. [](#new)\n    * **A dependency can now name the generation of Grav it is for.** A plugin that supports both 1.7 and 2.0 often needs a different version of the same dependency on each, so a `dependencies` entry takes an optional `grav` key: `- { name: form, version: '>=9.1.0', grav: '2.0' }`. Entries without it apply everywhere, so existing blueprints are unchanged. See [Plugin Compatibility](https:\/\/learn.getgrav.org\/20\/plugins\/plugin-compatibility#requiring-different-versions-per-grav-generation)\n    * A new `system.images.progressive_jpeg` setting, on by default, controls whether resized and cached JPEGs are saved as progressive\n\n1. [](#bugfix)\n    * Installing a package whose dependency is not in the GPM index now says so and carries on, instead of stopping the command with a PHP fatal error. A plugin that still asks for the Grav 1.7 admin plugin was enough to trigger it [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * A plugin that asks for the `admin` plugin now has that read as Admin 2 on Grav 2, so a plugin written for both 1.7 and 2.0 installs instead of failing on a dependency that cannot exist there. The version it asks for is not carried over, because it describes the old admin's numbering [getgrav\/grav-premium-issues#618](https:\/\/github.com\/getgrav\/grav-premium-issues\/issues\/618)\n    * Any other dependency that cannot be installed on this generation of Grav is now left out of the install rather than attempted and failed\n    * A JSON request whose body is a bare scalar (`\"text\"`, `12345`, `true`) sent with `Content-Type: application\/json` no longer answers a 500 from the request pipeline before any route runs. It is treated as an empty body, so a plugin's webhook or API route gets to answer it, log it and refuse it itself. With `errors.display` on, the old failure also printed a stack trace with server paths to whoever sent it\n    * An image default such as `resize` set in `system.images.defaults` works again. Every image manipulation was being skipped since 2.0.22, leaving only the loading and decoding hints [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Settings such as `loading: lazy` now stay on the image when `link` is also on, instead of moving onto the surrounding link where the browser never sees them [#4282](https:\/\/github.com\/getgrav\/grav\/issues\/4282)\n    * Resized and cached JPEGs are saved as progressive again, so a photo appears as a whole blurry image that sharpens instead of filling in one line at a time. It has been Grav's default since 2014 but silently stopped working in 1.4.6 [#4284](https:\/\/github.com\/getgrav\/grav\/issues\/4284)\n    * A media file's own settings from `media.yaml` are no longer overwritten the moment the image is opened, so custom default filters for an image type work again\n    * A URL typed with a trailing slash no longer errors out on a site that has the debugger switched on and the Login plugin protecting page media, because the debug bar is now skipped on redirects, where there is no page to put it on [#4280](https:\/\/github.com\/getgrav\/grav\/issues\/4280)\n    * With the optional `system.session.read_and_close` setting turned on, a change made to the session early in a request is no longer thrown away by a later write in that same request, and a message added just before a redirect now reaches the page it was meant for [#4281](https:\/\/github.com\/getgrav\/grav\/issues\/4281)\n"},"2.0.23":{"date":"09\/02\/2026","content":"1. [](#improved)\n    * `composer.json` now declares the `ctype` and `session` extensions it has always used, and suggests `fileinfo` and `simplexml`. Installing with `composer create-project` on a machine missing one of these no longer quietly walks back to a years-old release of Grav instead of failing [#4273](https:\/\/github.com\/getgrav\/grav\/discussions\/4273)\n1. [](#bugfix)\n    * Reading the browser name, platform or version no longer raises a PHP deprecation notice when a request arrives with no user agent, which is every request from a bare script or a health check\n    * Opening Clockwork before anything has been profiled, on a fresh install or right after `bin\/grav clear`, now reports that there is no data yet instead of failing with a 500\n    * A `GRAV_CONFIG__` override set to `true` or `false` now reaches the configuration as a real yes\/no value instead of the word itself, so switching something off from a `.env` file or the server environment actually switches it off. Thanks to @nerdyjan for the report and @AdilAzhariOmsan for the fix [#4277](https:\/\/github.com\/getgrav\/grav\/issues\/4277)\n    * `Uri::ip()` now reads the visitor's address from `$_SERVER`, falling back to the environment, so hosts that don't hand request variables to PHP's environment no longer report every visitor as `UNKNOWN`. Anything that counts per address there, such as the Login plugin's failed-login lockout and per-IP rate limiting, had been sharing a single bucket. Thanks to @sandymac [#2507](https:\/\/github.com\/getgrav\/grav\/issues\/2507)\n    * On those same hosts the `system.http_x_forwarded` options for `ip`, `client_ip` and `cf_connecting_ip` had no effect at all, and now work as documented. If you turned one on and saw nothing change, turn it back off unless the site really is behind a proxy that overwrites that header\n"},"2.0.22":{"date":"08\/31\/2026","content":"1. [](#new)\n    * The `url()` Twig function now takes a language, so a link to a route that isn't a page - a search page, a form action - can carry the site's language prefix: `{{ url('\/search', lang=true) }}`\n    * Blueprints can use a `media` field type, which saves a picked file as its path and keeps a list of them when the field allows more than one\n    * Page collections can now exclude one or more template types with `notOfType()`, the counterpart to the existing `ofType()` [#3910](https:\/\/github.com\/getgrav\/grav\/issues\/3910)\n    * The scheduler can now run the jobs that have missed their scheduled time, rather than only the ones due this very minute. Run it with `bin\/grav scheduler --catch-up`, which is what you want on a site that has no cron entry set up\n1. [](#improved)\n    * Twig in page content can no longer read the site's configuration through the `print_r`, `vardump`, `json_encode`, `yaml_encode` and `string` filters. The check that was meant to stop it had been asking whether the whole site was in sandbox mode, which Grav never does \u2014 it decides per template \u2014 so it had been letting everything through. Thanks to @Vectrain51\n    * The `|map`, `|filter` and `|reduce` Twig filters now check for themselves whether the template calling them is sandboxed, rather than relying on Twig to work it out. Thanks to @DhiyaneshGeek\n    * The scheduler no longer rebuilds a queued job from an unsigned queue file, so a file written into the queue folder by something other than Grav can at most re-run a job the site was already set up to run. The page index also refuses to build objects while reading its cache. Thanks to @elite0529\n    * Plugin and theme descriptions are now rendered in Parsedown's safe mode before admin displays them, so a description carrying raw HTML shows as text rather than being rendered. Thanks to @alham-rizvi\n    * The debugger's Clockwork data endpoint now answers only requests coming from the server itself, or requests presenting the secret set in the new `debugger.token` option. Cookies and API tokens are no longer recorded in profiler data whatever the `censored` option is set to\n    * Building a URL is now faster, which adds up over the hundreds of asset and link URLs a single page render produces\n    * The scheduler now records whether a run was started by cron or by hand, and a run you started yourself no longer counts as evidence that cron is set up\n    * `bin\/grav scheduler -r` now records the run against each job, the same as a scheduled run, so the next run knows what has already happened\n    * A field that is rejected only for being too long or too short now says so, and gives both the length submitted and the limit, instead of the same \"Invalid input\" any other bad value gets\n    * Multiline fields no longer carry a length limit low enough to affect real writing. Set `max: 0` on a field to remove the limit altogether\n1. [](#bugfix)\n    * The `|reduce` Twig filter now actually reduces. It was running the `|map` code by mistake and throwing away the starting value, so `[1,2,3]|reduce((c, v) => c + v, 0)` gave back a list instead of `6`. Thanks to @DhiyaneshGeek\n    * A form field's `minlength` and `maxlength` are now checked when the form is submitted, not only by the browser. They were being written into the page as HTML attributes but ignored on the server, so anything that skipped the browser's own check went straight through [#642](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/642)\n    * A field with a `step` set now accepts the lengths and counts that land on a step, and rejects the ones that do not. The check was the wrong way round, so it rejected exactly the values it was meant to allow\n    * A long page can be saved from the admin again. Page content was capped at 65,536 characters, so anything longer than roughly twenty pages of text was refused, and the only way to edit it was to write the file directly [#3643](https:\/\/github.com\/getgrav\/grav\/issues\/3643)\n    * A site installed in a subfolder no longer mangles URLs whose path repeats the install folder's name, such as an image at `\/images\/subdir\/photo.jpg` on a site installed at `\/subdir`\n    * A link to a page that carries a query string or an anchor, such as `\/blog?page=2`, now resolves to the page and keeps its language prefix, instead of being passed through as a plain path\n    * On a site installed in a subfolder, links written with the full path now resolve to the page, so they pick up the site's language and page extension\n    * A cache folder that the web server cannot write to no longer takes the whole site down. Grav now logs a warning naming the folder and serves the request without the cache, so the front end and the admin both stay reachable and the Problems plugin can report what is wrong [#4260](https:\/\/github.com\/getgrav\/grav\/issues\/4260)\n    * The same failure writing `user\/config\/versions.yaml` no longer stops the site either [#3688](https:\/\/github.com\/getgrav\/grav\/issues\/3688)\n    * Errors about a file that cannot be written now name the folder and say whether it is missing or not writable, instead of only reporting the file\n    * A session cookie name starting with `__Secure-` or `__Host-` now keeps that prefix and is sent with the settings browsers require for it, so the extra protection those prefixes give actually applies. Thanks to @wakqasahmed for the fix [#3773](https:\/\/github.com\/getgrav\/grav\/issues\/3773)\n      Note: sites whose `system.session.name` contains capitals, underscores or a leading or trailing dash will get a slightly different cookie name after this update, which signs their users out once.\n    * The scheduler no longer reports that cron is not set up when the crontab entry is written in a valid but slightly different style, such as one using `&&` or an absolute path to `bin\/grav`\n    * On a site with a custom scheduler job of its own, looking up a job by name no longer misses every job the system and its plugins register, so the backup and cache jobs can be found and run individually\n    * A scheduler job that finished its work but could not then write its output file, send its notification email or run its callback no longer aborts the whole run. The remaining jobs run, every result is still recorded, and the problem is written to the log\n    * A scheduler job registered without a schedule of its own no longer causes an error when its next run time is worked out\n    * `bin\/grav scheduler -j` no longer fails on a site with jobs registered by a plugin, and `-d` no longer fails on a job that has never run\n    * A scheduler job that runs one of Grav's own command line scripts now works when the scheduler is triggered from the web rather than from cron. Those jobs used to fail with \"env: php: No such file or directory\", because the web server does not have php on its path\n    * A scheduler job registered as a whole command line, such as `bin\/plugin myplugin sync`, now runs. Only the executable and its arguments given separately used to work, so a job written the other way looked for a file whose name contained spaces and failed every time it ran\n    * A site served from a subpath by a proxy no longer loses that subpath when a trailing slash is redirected, which previously sent visitors outside the site. The homepage of such a site also no longer redirects to the bare domain. Thanks to @wakqasahmed for the fix [#3822](https:\/\/github.com\/getgrav\/grav\/issues\/3822)\n    * With `force_ssl` turned on, a page that does not exist now redirects to HTTPS like every other page, instead of serving the 404 over plain HTTP. Thanks to @wakqasahmed for the fix [#3703](https:\/\/github.com\/getgrav\/grav\/issues\/3703)\n    * Image settings are no longer applied to audio, video, SVG or document media. An embedded MP3 kept its player instead of being turned into a linked thumbnail, and media URLs no longer pick up stray `loading`, `decoding` and `fetchpriority` values, which happened on every site whether or not those settings had been changed. Thanks to @wakqasahmed for the fix [#4264](https:\/\/github.com\/getgrav\/grav\/issues\/4264)\n"},"2.0.21":{"date":"08\/22\/2026","content":"1. [](#bugfix)\n    * Form fields no longer print their HTML attributes as text above the field, a problem the Twig update in 2.0.20 introduced on every form [#4256](https:\/\/github.com\/getgrav\/grav\/issues\/4256)\n    * A custom text escaper registered by a plugin now works again, instead of stopping the page with an error the first time a template used it\n"},"2.0.20":{"date":"08\/21\/2026","content":"1. [](#improved)\n    * Updated the bundled Twig fork to the current 3.x, picking up the correctness and sandbox improvements from the 3.27 and 3.28 releases.\n    * Grav now runs on Twig 3.28 and newer, which tightened the escaping method that Grav's compatibility shim replaces and would otherwise stop the site with a server error.\n    * The Twig content sandbox now accepts the list of tests a template uses, which newer Twig versions hand to it and will require from Twig 4.\n    * The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.\n    * Script and style files whose name already contains a version, such as those the Admin panel ships, are cached permanently in that same configuration, because a change always produces a new name.\n1. [](#bugfix)\n    * [security] Page content can no longer register a script or stylesheet through the Twig content sandbox, and asset URLs are now escaped where the tag is built, closing a way to inject markup into a rendered page.\n    * [security] The `read_file` capability no longer includes the user data folder by default, so page content can no longer be used to publish form submissions and other stored data.\n    * [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.\n    * [security] Custom Twig sandbox denial rules now take effect regardless of how the class name is capitalised, and can no longer be silently bypassed through a parent class or interface.\n    * A damaged page cache file is now rebuilt from the original page instead of stopping the site with a server error [#4239](https:\/\/github.com\/getgrav\/grav\/issues\/4239)\n    * Images and links in page content now work when the file name contains a colon, such as a screenshot named after a timestamp [#3933](https:\/\/github.com\/getgrav\/grav\/issues\/3933)\n    * A page that sets a full web address as its canonical route now uses that address on its own, instead of joining it onto the site's own address and breaking sitemaps and canonical links [#4023](https:\/\/github.com\/getgrav\/grav\/issues\/4023)\n    * Turning on asset timestamps now gives each stylesheet and script its own marker taken from when that file last changed, so editing one file no longer waits on an unrelated change before visitors see it [#4049](https:\/\/github.com\/getgrav\/grav\/issues\/4049)\n    * A canonical route set through the Flex pages API is now saved as written, instead of being stored in a form it could never be read back from.\n    * Flex directory blueprints no longer lose the fields the Flex Objects plugin adds when something reads the directory early in a request [#160](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/160)\n    * The scheduler's generated cron command now names the site's environment when that environment has its own configuration, and each run records which environment it used, so custom jobs defined in `user\/env\/<host>\/` no longer fail silently from cron [#4248](https:\/\/github.com\/getgrav\/grav\/issues\/4248)\n    * Audio and video players generated by `media.html()` no longer carry an `alt` attribute, which isn't valid on those elements; any alternative text is kept as an accessible label instead, so the markup passes validation [#3540](https:\/\/github.com\/getgrav\/grav\/issues\/3540)\n"},"2.0.19":{"date":"08\/14\/2026","content":"1. [](#new)\n    * You can now tighten the Twig content sandbox below its built-in defaults with new `denied_*` settings in `security.yaml`.\n    * The \"Twig in Content\" report can show the effective sandbox policy, so you can see exactly what page content is allowed to do.\n1. [](#improved)\n    * A theme or plugin that ships its own `.htaccess` can no longer switch off the protection on its own folder, which used to leave its configuration and template files downloadable [#4236](https:\/\/github.com\/getgrav\/grav\/issues\/4236)\n    * Twig in page content now renders on new installs by default, instead of appearing as raw text until the setting was turned on.\n    * The long Twig sandbox allowlists now ship built into Grav, so `security.yaml` only records your own additions and future security updates to the defaults reach every site.\n    * Existing sites that had trimmed those allowlists to tighten them keep exactly that policy after upgrading, now recorded as explicit `denied_*` entries.\n    * Removed two rarely-used Twig sandbox switches (`logging` and `admin_hint`); both behaviours are now always on.\n1. [](#bugfix)\n    * The content cross-site scripting check no longer objects to harmless `<option>` and `<select>` markup, whose original issue is fixed in the form field that actually rendered it.\n    * Sites running with the Twig 2 compatibility setting no longer crash with a server error on every page once an update clears the template cache [#4235](https:\/\/github.com\/getgrav\/grav\/issues\/4235)\n"},"2.0.18":{"date":"08\/11\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.14, which closes two further ways a crafted stylesheet could hide an external image reference from the checks added in the previous release ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n    * [security] Modular pages are now checked for cross-site scripting when they are saved, closing a way for a page editor to store a script that ran for every visitor ([GHSA-fg8g-663r-f366](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fg8g-663r-f366)).\n    * [security] The Twig `sort` and `find` filters no longer run a plain function name as a callable inside the content sandbox, closing a way for a page editor to execute arbitrary PHP ([GHSA-p6qj-p5m7-f62h](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p6qj-p5m7-f62h)).\n    * A blueprint that builds on another one can again fill dropdowns from its own PHP, which mostly affected themes because their page blueprints nearly always extend the default one ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)).\n    * The Scheduler no longer fails outright on hosts that disable PHP's `proc_open`, so scheduled jobs can still be viewed and edited there [getgrav\/grav-admin-next#16](https:\/\/github.com\/getgrav\/grav-admin-next\/issues\/16)\n    * A scheduled job that cannot be started on such a host is now reported as failed with an explanation, instead of stopping the whole scheduler run\n    * Grav now works out who the site runs as without starting a shell, so that detail still appears when external commands are unavailable\n    * The record of when the scheduler last ran is now written to a fixed location rather than one relative to wherever the trigger happened to run from\n1. [](#improved)\n    * Grav now decides whether the scheduler is being triggered by checking that each job has run when its own schedule says it should have, instead of requiring a run in the last two minutes, so a sparse crontab, a webhook or a scheduled task on Windows all count\n"},"2.0.17":{"date":"08\/07\/2026","content":"1. [](#bugfix)\n    * [security] Updated the bundled DOM sanitizer to 1.0.13, which stops CSS comments from hiding dangerous values and covers image loading through `image-set()`, so untrusted SVG or HTML can no longer reference external resources those ways ([GHSA-ww22-4mqv-x5w3](https:\/\/github.com\/rhukster\/dom-sanitizer\/security\/advisories\/GHSA-ww22-4mqv-x5w3)).\n"},"2.0.16":{"date":"08\/07\/2026","content":"1. [](#improved)\n    * Updated vendor libs to latest versions\n    * The list of groups on the account form is now read through the current user groups system, retiring a routine deprecated since Grav 1.7.\n1. [](#bugfix)\n    * [security] The site, system and theme settings offered to Twig written inside page content are now filtered by the same denied-paths list that already covered `config`, so a page editor can no longer read secrets such as a Redis password straight out of them ([GHSA-p597-crqc-m349](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-p597-crqc-m349)).\n    * [security] Twig written into a form's email settings now runs under the same restrictions as Twig written into page content, closing a route that let someone with only page-editing rights run commands on the server ([GHSA-gh8j-q67c-j53f](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-gh8j-q67c-j53f)).\n    * [security] Form security tokens are now compared with a routine that takes the same amount of time whichever characters differ, so the check can no longer hint at how much of a guess was right ([GHSA-38p6-h87p-r4cg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-38p6-h87p-r4cg)).\n    * [security] The check for whether a visitor arrived from your own site now requires a full address match, so another site whose domain merely begins with yours no longer counts as your own ([GHSA-9ccq-2jfg-qw33](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-9ccq-2jfg-qw33)).\n    * [security] Scheduler job locks are now kept inside your site rather than in the shared system temp folder, so another account on the same server can no longer redirect a lock write to a file of its choosing ([GHSA-q8w8-6cq5-j4h2](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q8w8-6cq5-j4h2)).\n    * A scheduled job set to run only one at a time now refuses to start when its lock cannot be written, instead of running unprotected. Note that `bin\/grav clear` removes these locks along with the rest of the temporary folder.\n    * [security] Deleting, renaming and copying a media file now check the whole path rather than just the file's own name, so a plugin calling those routines directly cannot reach a file outside the media folder ([GHSA-jq29-c7v8-rg55](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-jq29-c7v8-rg55)).\n    * [security] The `media_directory()` Twig function now only accepts folders inside your site, so Twig written into page content can no longer list files or republish images from elsewhere on the server ([GHSA-47ch-6w46-6xm7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-47ch-6w46-6xm7)).\n    * Deleting a media file no longer also removes the retina copies and metadata belonging to a different file whose name merely ends with the same text, so deleting `banner.jpg` leaves `my-banner@2x.jpg` alone.\n    * A fallback page handed to `Uri::referrer()` is now returned as given, instead of being trimmed away to nothing whenever the visitor did not arrive from your site.\n    * `Pages::referrerRoute()` was comparing a full web address against a site path, so it never recognised a match and always reported that the visitor came from somewhere else.\n"},"2.0.15":{"date":"08\/03\/2026","content":"1. [](#bugfix)\n    * A plugin's blueprints can use the data providers that plugin ships again, instead of having every one of them refused by a check that only recognised the providers core itself registers ([getgrav\/grav-plugin-email#193](https:\/\/github.com\/getgrav\/grav-plugin-email\/issues\/193)). Fields defined in page frontmatter are held to the stricter rule instead, which is where the risk actually was.\n    * [security] A configuration admin can no longer reach an unvetted built-in routine by writing a blueprint field's data provider as a plain function name, a spelling that was still being checked against a list of known-bad names rather than the list of approved ones ([GHSA-f8wv-xp27-6gq7](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-f8wv-xp27-6gq7)).\n    * [security] The content security scan now reads an unpaired quote inside an unquoted attribute value the way a browser does, closing another way a page editor could hide a script from it ([GHSA-vfmf-q6x9-cw96](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-vfmf-q6x9-cw96)).\n    * [security] The media URL in an audio or video tag is now escaped, so a filename carrying markup can no longer add its own attributes to the player ([GHSA-6qw9-4vv5-jr97](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-6qw9-4vv5-jr97)).\n    * JSON responses no longer fail outright when the data contains invalid UTF-8. `json_encode()` returns `false` on malformed bytes, and the PSR-7 response body is type-hinted `string|resource|StreamInterface`, so that `false` came back out as an unhandled `TypeError` from inside the vendor stream rather than as a response. Affected `createJsonResponse()` and both JSON error responses in `ControllerResponseTrait`, where an exception message carrying a bad byte would take out the error handler itself, plus the Clockwork data endpoint in `Debugger`. Bad bytes are now substituted, and the remaining structural failures (recursion depth, `INF`\/`NAN`) raise a catchable `JsonException` instead of a silent `false`. Output for valid data is unchanged.\n    * [security] The fast static asset server now keeps a request inside the directory the site published, instead of also allowing any neighbouring directory whose name starts with the same letters ([GHSA-4v9q-p283-qc2m](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-4v9q-p283-qc2m)).\n    * [security] File uploads now reject a few more extensions that browsers run script from, or that a server may hand to PHP: `xhtml`, `xht`, `svgz`, `php7`, `php8`, `pht`, `phtm` and `phps` ([GHSA-66xf-ggf4-6hmc](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-66xf-ggf4-6hmc)).\n    * [security] The bundled `Caddyfile` protections did nothing. They were written as nginx-style regexes, which Caddy reads as literal paths that never match, and the `respond` they redirected to ran after the catch-all rewrite had already claimed the request. A site served with this config handed out `user\/accounts\/`, `user\/config\/`, `logs\/`, `composer.lock`, page files, and the `system\/` and `vendor\/` folders to anyone who asked. The rules are now named `path_regexp` matchers answering `403` directly, inside a `route` block so they run before the rewrite, and they were checked request by request against the `.htaccess` behaviour.\n"},"2.0.14":{"date":"07\/30\/2026","content":"1. [](#improved)\n    * Removed old JQuery `2.1.4`. However `2.2.4` and `3.7.1` remain for legacy support\n1. [](#bugfix)\n    * [security] An administrator with account-management rights can no longer grant themselves super-admin access by saving it into a user group's permissions, a protection the account form already had ([GHSA-xhfv-7758-r9hx](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xhfv-7758-r9hx)).\n    * [security] The content security scan no longer reports a page as clean when it was unable to examine it, closing two ways a page editor could hide a script from it: a single invalid character anywhere in the content, and padding a tag out to several thousand characters ([GHSA-q2j8-x8hf-63ch](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-q2j8-x8hf-63ch)).\n"},"2.0.13":{"date":"07\/25\/2026","content":"1. [](#new)\n    * Added an `array_group_by` Twig filter and function for grouping a list of items by one of their values ([#4218](https:\/\/github.com\/getgrav\/grav\/pull\/4218)).\n1. [](#bugfix)\n    * [security] A configuration admin can no longer run code on the server by pointing a dynamic field's data provider at a built-in routine named as a class-and-method pair, a form that slipped past the safety check because it only inspected the single-string spelling; both forms are now vetted the same way ([GHSA-r94f-hx44-8jqf](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-r94f-hx44-8jqf)).\n    * [security] A page editor without super-admin rights can no longer store an event-handler script that runs for site visitors by hiding it behind a `>` placed inside a quoted HTML attribute; the content security scan now reads quoted attribute values the same way a browser does ([GHSA-269c-h76q-8cxw](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-269c-h76q-8cxw)).\n    * [security] A backup profile's location is now confined to the site folder, so a profile pointing outside the Grav root can no longer pull external directories into the backup archive ([GHSA-fch7-cpv4-w7hg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fch7-cpv4-w7hg)).\n    * [security] Uploaded filenames may no longer contain the HTML characters `<`, `>`, or `\"`, so a stored filename cannot carry markup that could run if it were later shown unescaped.\n    * [security] The `find` and `sort` Twig filters now reject a dangerous function name given as their callback, matching the protection already applied to `map`, `filter`, and `reduce`, so template values cannot use them to run code ([GHSA-xx48-97m4-h7qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xx48-97m4-h7qm)).\n    * The bundled `nginx.conf` security rules are now anchored to the start of the path like the `.htaccess` rules already are, so the admin's Tools \u2192 Logs viewer works on nginx instead of being blocked ([#4223](https:\/\/github.com\/getgrav\/grav\/pull\/4223)).\n    * On non-FastCGI setups Grav no longer sends an invalid `Content-Encoding: none` header, which some strict HTTP clients rejected outright; it now closes the connection cleanly without the bogus value ([#2619](https:\/\/github.com\/getgrav\/grav\/issues\/2619)).\n"},"2.0.12":{"date":"07\/20\/2026","content":"1. [](#new)\n    * Added per-language fallbacks for unsupported browser languages during `HTTP_ACCEPT_LANGUAGE` negotiation, allowing them to resolve to supported languages without exposing additional language routes.\n1. [](#improved)\n    * The `read_file()` Twig function can now read `.css` files by default, so inline stylesheets can be embedded in a template without adjusting the security config ([#4215](https:\/\/github.com\/getgrav\/grav\/issues\/4215)).\n    * Documented that the site-wide media object resolves lazily, so its inherited collection query methods need `media_directory()` to filter or sort site media ([#4210](https:\/\/github.com\/getgrav\/grav\/issues\/4210)).\n1. [](#bugfix)\n    * Browser language codes are now matched case-insensitively during `HTTP_ACCEPT_LANGUAGE` fallback negotiation.\n"},"2.0.11":{"date":"07\/13\/2026","content":"1. [](#new)\n    * You can now filter, sort, and group a page's media by the values in their `.meta.yaml` metafiles directly in Twig, with new `filterBy`, `where`, `findBy`, `sortBy`, `groupBy`, and `withMeta` methods on `page.media`. Fixes [getgrav\/grav#4200](https:\/\/github.com\/getgrav\/grav\/issues\/4200).\n1. [](#bugfix)\n    * [security] A page editor can no longer read arbitrary files from the server by pointing an image watermark at a traversal path such as `carrier.png?watermark=..\/secret.png`; an editor-supplied watermark path is now confined to the site's media, while operator-configured watermarks and stream URIs are unaffected ([GHSA-w3f4-8pj2-599w](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-w3f4-8pj2-599w)).\n    * [security] A page-edit account can no longer reach file-disclosure or secret-read functions by naming an arbitrary `Class::method` as a dynamic field's data provider; qualified providers are now limited to a known-safe allowlist, closing a bypass of the guard added in 2.0.7 and 2.0.9 ([GHSA-7pgq-cr25-xvc8](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-7pgq-cr25-xvc8), [GHSA-cxv3-5jj3-cpgr](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-cxv3-5jj3-cpgr)).\n    * A page is no longer blanked when viewed just because a trusted plugin or shortcode on it outputs markup the content security scan flags, such as an embed, form, or icon; the check that guards against dangerous editor content now runs once when the page is saved rather than every time it is rendered ([GHSA-2c4f-86xc-cr74](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-2c4f-86xc-cr74)).\n1. [](#improved)\n    * [security] Page content that uses Twig to assemble disallowed markup at render time, such as building an event handler or a `<script>` tag from separate pieces, is now refused when you save the page instead of being allowed through to visitors.\n    * The `raw` Twig filter is no longer allowed inside editor-authored page content, so page content can no longer output unescaped dynamic values past the content security check; trusted theme templates are unaffected.\n"},"2.0.10":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * A partial `system.pages.process` override in `user\/config\/system.yaml` (for example setting only `twig: false`) no longer silently turns off Markdown and leaves every page rendering its raw source. Because `pages.process` is a single field in the blueprint, a partial override replaced the whole map and dropped the default `markdown: true`; core now re-applies that default, so an affected site recovers on update with no change to its config. An explicit `markdown: false` is still honored.\n"},"2.0.9":{"date":"07\/09\/2026","content":"1. [](#bugfix)\n    * [security] A callable directive hidden in a Flex directory's blueprint field can no longer run commands on the server; dynamic field data on Flex types (Flex Pages, Flex Users, Flex Objects) now refuses dangerous functions, closing a path that bypassed the same guard added for page forms in 2.0.7 ([GHSA-c4wf-2xxc-68qm](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-c4wf-2xxc-68qm)).\n    * [security] The scheduler webhook trigger now fails closed when no token is configured, so an enabled webhook left without a token can no longer run scheduled jobs for anonymous callers ([GHSA-xwv3-2mv2-w33x](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-xwv3-2mv2-w33x)).\n    * Images and links whose filename contains spaces now render when the path is wrapped in angle brackets, for example `![](<My image.jpg>)`. Fixes [getgrav\/grav#4197](https:\/\/github.com\/getgrav\/grav\/issues\/4197).\n"},"2.0.8":{"date":"07\/06\/2026","content":"1. [](#bugfix)\n    * An email or `www.` URL used as the visible text of a Markdown link is no longer turned into a second, nested link when GFM autolinks are enabled. Fixes [getgrav\/grav#4191](https:\/\/github.com\/getgrav\/grav\/issues\/4191).\n    * `md5()` can once again be called as a Twig function, not just as the `|md5` filter, so themes and plugins that generate an id or cache-busting hash with `md5(...)` keep working instead of failing with an \"Unknown function\" error. Fixes [getgrav\/grav-theme-quark2#12](https:\/\/github.com\/getgrav\/grav-theme-quark2\/issues\/12).\n    * Replacing an image in place, such as swapping the image in a Flex object field, now shows the new image on the site instead of the previously cached version (updated `getgrav\/image` to v4.1.3, which includes the source file's modification time and size in the derivative cache key). Fixes [getgrav\/grav#4195](https:\/\/github.com\/getgrav\/grav\/issues\/4195).\n"},"2.0.7":{"date":"07\/04\/2026","content":"1. [](#bugfix)\n    * [security] A page editor can no longer run commands on the server by hiding a callable directive in a form field's settings; dynamic field data now refuses dangerous functions and cannot be tricked into reaching one through a helper ([GHSA-fj2p-qj2f-74v5](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-fj2p-qj2f-74v5)).\n    * A page's `translatedLanguages()` now localizes ancestor slugs too, so a nested translation whose parent folder has a localized `slug:` produces the fully translated cross-language link instead of leaving parent segments in the current language. Fixes [getgrav\/grav#4186](https:\/\/github.com\/getgrav\/grav\/issues\/4186).\n    * Pointing the log stream at `environment:\/\/` (for example `log: environment:\/\/logs`) no longer crashes the site or `bin\/grav clear` with a \"stream must either be a resource or a string\" error when the per-environment folder does not exist; logging now falls back to the default `logs\/` folder instead. Fixes [getgrav\/grav#4172](https:\/\/github.com\/getgrav\/grav\/issues\/4172).\n    * The `media:\/\/` stream now checks the per-environment `user\/env\/<host>\/media\/` folder before the shared `user\/media\/`, so site media stored per environment resolves to the correct URL in the admin and in page content instead of a broken `user\/media\/` link. Fixes [getgrav\/grav#4188](https:\/\/github.com\/getgrav\/grav\/issues\/4188).\n    * Large file downloads such as site backups are now streamed to the browser in chunks instead of being loaded into memory all at once, so a download bigger than PHP's memory limit no longer fails with a blank server error. Fixes [getgrav\/grav-plugin-api#12](https:\/\/github.com\/getgrav\/grav-plugin-api\/issues\/12).\n    * Backups initialization no longer runs twice when something that bypasses the normal request middleware (such as the API plugin) also attaches the backup scheduler listener, so the listener is registered only once.\n    * Pages accessed with URL parameters such as pagination or taxonomy filters no longer recompile every Twig template on each request, restoring full template caching on exactly the pages that get the most traffic.\n    * The per-file compiled cache for YAML and markdown files now loads through its intended opcache fast path, and a source file that has been deleted no longer serves stale cached data.\n    * A modular page that outputs trusted theme or plugin markup, such as a form with a reCAPTCHA field, is no longer wrongly blanked by the content security scan, which now checks the editor's own content instead of the finished template output. Fixes [getgrav\/grav-plugin-form#636](https:\/\/github.com\/getgrav\/grav-plugin-form\/issues\/636).\n    * Chaining media actions on page media under the content security scan, such as `{{ page.media['x.jpg'].lightbox(1024,768).cropResize(176,176).html() }}`, now works instead of being blocked, and the scan's list of allowed media methods stays in step with Grav's documented media actions automatically.\n1. [](#improved)\n    * Updating a plugin or theme whose required dependency is held back by a newer Grav or PHP requirement now explains the real fix. Instead of reporting that the needed version is \"higher than the latest release\" and suggesting a cache refresh, the updater names the newer dependency release and the Grav (or PHP) version it needs, so you know to update Grav first. Relates to [getgrav\/grav-plugin-admin2#93](https:\/\/github.com\/getgrav\/grav-plugin-admin2\/issues\/93).\n    * Backup profiles now always appear in the scheduler where each can be switched on or off with the Enabled\/Disabled toggle, instead of a profile staying hidden until its schedule was turned on; the profile's schedule setting seeds the default state and an explicit toggle takes precedence.\n    * Frontend requests are noticeably faster across the board: the scheduler, backups machinery, error page renderer and logger now initialize only when actually used instead of on every page view, cutting over 50 PHP files from a typical request.\n    * The filesystem scan that checks pages for changes now reuses its result for a couple of seconds (configurable with `cache.check.interval`), so busy sites no longer stat every page file on every single request; content edits still show up right away in normal editing workflows and admin saves remain instant.\n    * Configuration, blueprint and language file lists honor the same freshness window instead of checking every tracked file's timestamp three times per request, and theme blueprints no longer load at all on normal frontend page views.\n    * Class autoloading is faster: source installs get an optimized class map, APCu is suggested so the existing autoloader cache setting can engage, and plugin autoloaders no longer sit in front of the core one where every core class lookup had to pass through them first.\n    * Rendering a page with cached content no longer loads its whole media collection up front, the pages index no longer stores pre-computed metadata for every page, and relative markdown links resolve their target page directly instead of building the full page list per link.\n    * Assorted hot-path trims: string helpers use fast native functions for the common case, asset rendering skips per-asset integrity work when the feature is off, the site root URL is computed once per request instead of per asset, and debugger timers cost nothing when the debugger is disabled.\n    * New experimental opt-in page index (`pages.lazy_index: true`): pages, routes, children lists, sort orders and the taxonomy map load on demand from a per-page index instead of one large cache blob that has to be fully unserialized on every request, making per-request cost flat as sites grow: a 2,000 page test site renders as fast as a 2 page one and uses a quarter of the memory; SQLite powers the index when available with a pure PHP fallback, and the default behavior is completely unchanged until the flag is enabled.\n    * Page collection filters (`visible`, `routable`, `published`, module) now use menu flags recorded in the page index, so building a navigation menu that filters a folder to its visible pages no longer loads every hidden sibling first. On a 500 post blog under the Quark theme this cut the pages built for a page view from all 507 to 7 and roughly halved memory; it helps every site, most of all large ones with the experimental page index enabled.\n    * Sorting a page collection by date, title, or another common field now reads that value from the page index instead of loading every page in the collection just to read one field, and on single language sites the automatic translated filter that every collection applies no longer loads any pages at all. With the experimental page index enabled, a blog post showing a related posts grid dropped from loading every post on the site to only the handful it displays.\n    * The setting that scanned rendered page content for XSS is retired, as the gap it covered is now closed when the page is saved rather than every time it is viewed; the old toggle is removed from your security configuration automatically on upgrade.\n"},"2.0.6":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * [security] Flex user avatars stored under `user\/accounts\/<username>\/` (folder storage) are now served too; the 2.0.5 avatar carve-out only covered the flatfile `user\/accounts\/avatars\/` layout, so folder-storage avatars kept returning a 403. Existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"2.0.5":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * A page's `translatedLanguages()` now returns each language's own route, so a translation with a localized `slug:` produces the correct cross-language link instead of repeating the default language's URL. Fixes [getgrav\/grav#4183](https:\/\/github.com\/getgrav\/grav\/issues\/4183).\n    * [security] Profile avatars display again instead of returning a 403; the folder hardening that locked down `user\/accounts` now makes a narrow exception for avatar images while account data such as password hashes stays blocked, and existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n    * Loading a page no longer fails with a \"Failed to write cache file\" error when Grav can't save the compiled template cache, such as on a shared folder, a full disk, or during a save-then-reload race; the page still renders and the problem is logged instead. Fixes [getgrav\/grav#4184](https:\/\/github.com\/getgrav\/grav\/issues\/4184).\n"}}},"1.7":{"version":"1.7.53.3","date":"2026-08-31T03:52:16Z","url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/1.7.53.3","min_php":"7.2.3","assets":{"grav-admin":{"name":"grav-admin-v1.7.53.3.zip","type":"binary\/octet-stream","size":19290359,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/1.7.53.3?testing","content_sha256":"db740daf94780c29f9714759590557c657656ee1f44ed2489518804132c6c485","sha256":"18d249a4a8517b456d6abafb550488f966324d7ef171d08d0a338068525bd39f"},"grav-update":{"name":"grav-update-v1.7.53.3.zip","type":"binary\/octet-stream","size":9824469,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/1.7.53.3?testing","content_sha256":"bb5b53737439feaa8d6d94b780e681fb46ddf79233fdfa03cacd63deda2d4a42","sha256":"3e53bde6e3c971151f13520306a2751106a948ce93bc77e0fc0e411302595318"},"grav":{"name":"grav-v1.7.53.3.zip","type":"binary\/octet-stream","size":12258277,"download":"https:\/\/getgrav.org\/download\/core\/grav\/1.7.53.3?testing","content_sha256":"87f9b1078706e58f4135abcee8fbb1f0e9add9e05e5cab6f8a920c2924acc411","sha256":"11dfedf39caa01a4e69f4a0f333ac61c5b89936bf1b270de0161530f3084fe86"}},"changelog":{"1.7.53.3":{"date":"08\/31\/2026","content":"1. [](#improved)\n    * The debugger's Clockwork data endpoint now answers only requests coming from the server itself, or requests presenting the secret set in the new `debugger.token` option. Cookies and API tokens are no longer recorded in profiler data whatever the `censored` option is set to\n    * A field that is rejected only for being too long or too short now says so, and gives both the length submitted and the limit, instead of the same \"Invalid input\" any other bad value gets\n    * Multiline fields no longer carry a length limit low enough to affect real writing. Set `max: 0` on a field to remove the limit altogether\n1. [](#bugfix)\n    * [security] Form security tokens are now compared with a routine that takes the same amount of time whichever characters differ, so the check can no longer hint at how much of a guess was right ([GHSA-38p6-h87p-r4cg](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-38p6-h87p-r4cg)).\n    * A long page can be saved from the admin again. Page content was capped at 65,536 characters, so anything longer than roughly twenty pages of text was refused, and the only way to edit it was to write the file directly [#3643](https:\/\/github.com\/getgrav\/grav\/issues\/3643)\n"},"1.7.53.2":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * [security] Flex user avatars stored under `user\/accounts\/<username>\/` (folder storage) are now served too; the 1.7.53.1 avatar carve-out only covered the flatfile `user\/accounts\/avatars\/` layout, so folder-storage avatars kept returning a 403. Existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"1.7.53.1":{"date":"06\/30\/2026","content":"1. [](#bugfix)\n    * The `user\/data` block added in 1.7.53 now makes an exception for public media uploads, such as Flex Object images, so they keep displaying instead of returning a 403, while data files, databases and keys stay blocked. Fixes [getgrav\/grav#4129](https:\/\/github.com\/getgrav\/grav\/issues\/4129).\n    * [security] Profile avatars display again instead of returning a 403; the folder hardening that locked down `user\/accounts` now makes a narrow exception for avatar images while account data such as password hashes stays blocked, and existing sites self-heal on upgrade. Fixes [getgrav\/grav#4185](https:\/\/github.com\/getgrav\/grav\/issues\/4185).\n"},"1.7.53":{"date":"06\/16\/2026","content":"1. [](#bugfix)\n    * [security] Direct web access to the `user\/accounts`, `user\/config`, `user\/data` and `user\/env` folders is now blocked outright in every bundled webserver config, closing a hole where files such as certificates, tokens and databases stored under `user\/data` with an unlisted extension could be downloaded directly.\n    * [security] A backup deny-all `.htaccess` now ships inside `user\/accounts`, `user\/config` and `user\/data` so Apache installs stay protected even when the site root `.htaccess` has been customised or is out of date.\n    * [security] The upgrade postflight now patches an existing stock root `.htaccess` to add the folder block automatically, so installs that updated from an earlier version are protected without editing the file by hand.\n    * [security] URL query image transforms (such as `image.jpg?resize=`) are now turned off by default and, when enabled, refuse oversized dimensions above a configurable pixel limit, closing an unauthenticated denial of service where huge resize values could exhaust server memory.\n"},"1.7.52":{"date":"04\/29\/2026","content":"1. [](#new)\n    * GPM client now sends the running PHP version with index requests so the server can substitute PHP-aware compat fallbacks when a plugin's latest release requires a newer PHP than the client can run.\n1. [](#bugfix)\n    * [security] Extended default `uploads_dangerous_extensions` to include `md`, `yaml`, `yml`, `json`, `twig`, `ini` \u2014 page-content extensions that can be weaponised via permissive form-upload `accept` policies (GHSA-w4rc-p66m-x6qq, defense-in-depth alongside the Form 9.1.0 plugin fix).\n"},"1.7.51":{"date":"04\/28\/2026","content":"1. [](#new)\n    * Added foundation for migrating to Grav 2.0: cross-major auto-upgrades are blocked in GPM, and core now surfaces a `next_major` hint so admin can point users at the new `migrate-grav` plugin\n    * Added `compatibility:` blueprint support so plugins\/themes can declare which Grav versions they support\n    * Added self-upgrade preflight that flags incompatible plugins\/themes and `psr\/log` \/ Monolog conflicts before proceeding\n    * Added upgrade resilience with automatic maintenance mode and opcache reset during self-upgrade\n    * Added new `cache-cleanup` CLI command to prune obsolete cache entries\n    * Added new `onFlexDirectoryConfigBeforeSave` event for Flex\n1. [](#improved)\n    * More readable time output in `bin\/grav logviewer` [#4009](https:\/\/github.com\/getgrav\/grav\/pull\/4009)\n    * Removed legacy standalone binary build\n    * Updated vendor libraries to latest versions\n1. [](#bugfix)\n    * Fixed `selectize` field losing values when keyed options were used\n    * Fixed wrong date output in `bin\/grav logviewer` [#4007](https:\/\/github.com\/getgrav\/grav\/pull\/4007)\n    * Fixed undefined array key error triggered by URL-encoded characters in paths [#4012](https:\/\/github.com\/getgrav\/grav\/pull\/4012)\n    * Fixed assorted issues in the revamped scheduler\n    * Fixed `schedule` flag not being honored in backup profiles\n    * Fixed default-language loading when using the session-based language store\n    * Allow `lang` query parameter to switch back to the default language\n"},"1.7.49.5":{"date":"09\/10\/2025","content":"1. [](#bugfix)\n    * Backup not honoring ignored paths [#3952](https:\/\/github.com\/getgrav\/grav\/issues\/3952)\n"},"1.7.49.4":{"date":"09\/03\/2025","content":"1. [](#bugfix)\n    * Fixed cron force running jobs severy minute! [#3951](https:\/\/github.com\/getgrav\/grav\/issues\/3951)\n"},"1.7.49.3":{"date":"09\/02\/2025","content":"1. [](#bugfix)\n    * Fixed an error in ZipArchive that was causing issues on some systems\n    * Fixed namespace change for `Cron\\Expression`\n    * Removed broken cron install field... use 'instructions' instead\n    * Fixed duplicate jobs listing in some CLI commands\n"},"1.7.49.2":{"date":"08\/28\/2025","content":"1. [](#bugfix)\n    * Fix translation of key for image adapter [#3944](https:\/\/github.com\/getgrav\/grav\/pull\/3944)\n"},"1.7.49.1":{"date":"08\/25\/2025","content":"1. [](#new)\n    * Rerelease to include all updated plugins\/theme etc.\n"},"1.7.49":{"date":"08\/25\/2025","content":"1. [](#new)\n    * Revamped Grav Scheduler to support webhook to call call scheduler + concurrent jobs + jobs queue + logging, and other improvements\n    * Revamped Grav Cache purge capabilities to only clear obsolete old cache items\n    * Added full imagick support in Grav Image library\n    * Added support for Validate `match` and `match_any` in forms\n1. [](#improved)\n    * Handle empty values on require with ignore fields in Forms\n    * Use `actions\/cache@v4` in github workflows\n    * Use `actions\/checkout@v4`in github workflows [#3867](https:\/\/github.com\/getgrav\/grav\/pull\/3867)\n    * Update code block in README.md [#3886](https:\/\/github.com\/getgrav\/grav\/pull\/3886)\n    * Updated vendor libs to latest\n1. [](#bugfix)\n    * Bug in `exif_read_data` [#3878](https:\/\/github.com\/getgrav\/grav\/pull\/3878)\n    * Fix parser error in URI: [#3894](https:\/\/github.com\/getgrav\/grav\/issues\/3894)\n\n"},"1.7.48":{"date":"10\/28\/2024","content":"1. [](#new)\n    * New Trait for fetchPriority attribute on images [#3850](https:\/\/github.com\/getgrav\/grav\/pull\/3850)\n1. [](#improved)\n    * Fix for #3164. Adds aliases as possible commands during lookup [#3863](https:\/\/github.com\/getgrav\/grav\/pull\/3863)\n1. [](#bugfix)\n    * Fix style conflict with Clockwork and tooltips [#3861](https:\/\/github.com\/getgrav\/grav\/pull\/3861)\n"},"1.7.47":{"date":"10\/23\/2024","content":"1. [](#new)\n  * New `Utils::toAscii()` method\n  * Added support for Clockwork Debugger to allow web UI (requires new `clockwork-web` plugin)\n1. [](#improved)\n  * Include modular sub-pages in last-modification date computation [#3562](https:\/\/github.com\/getgrav\/grav\/pull\/3562)\n  * Updated vendor libs to latest versions\n  * Updated JQuery to `3.7.1` [#3787](https:\/\/github.com\/getgrav\/grav\/pull\/3827)\n  * Updated vendor libraries to latest versions\n  * Support for Fediverse Creator meta tag [#3844](https:\/\/github.com\/getgrav\/grav\/pull\/3844)\n1. [](#bugfix)\n  * Fixes deprecated for return type in Filesystem with PHP 8.3.6 [#3831](https:\/\/github.com\/getgrav\/grav\/issues\/3831)\n  * Fix for `exif_imagtetype()` throwing an exception when file doesn't exist\n  * Fix JSON output comments check with content type [#3859](https:\/\/github.com\/getgrav\/grav\/pull\/3859)\n"},"1.7.46":{"date":"05\/15\/2024","content":"1. [](#improved)\n   * Better handling of external protocols in `Utils::url()` such as `mailto:`, `tel:`, etc.\n   * Handle `GRAV_ROOT` or `GRAV_WEBROOT` when `\/` [#3667](https:\/\/github.com\/getgrav\/grav\/pull\/3667)\n1. [](#bugfix)\n   * Fixes for multi-lang taxonomy when reinitializing the languages (e.g. LangSwitcher plugin)\n   * Ensure the full filepath is checked for invalid filename in `MediaUploadTrait::checkFileMetadata()`\n   * Fixed a bug in the `on_events` REGEX pattern of `Security::detectXss()` as it was not matching correctly.\n   * Fixed an issue where `read_file()` Twig function could be used nefariously in content [#GHSA-f8v5-jmfh-pr69](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-f8v5-jmfh-pr69)\n"},"1.7.45":{"date":"03\/18\/2024","content":"1. [](#new)\n   * Added new Image trait for `decoding` attribute [#3796](https:\/\/github.com\/getgrav\/grav\/pull\/3796)\n1. [](#bugfix)\n   * Fixed some multibyte issues in Inflector class [#732](https:\/\/github.com\/getgrav\/grav\/issues\/732)\n   * Fallback to page modified date if Page date provided is invalid and can't be parsed [getgrav\/grav-plugin-admin#2394](https:\/\/github.com\/getgrav\/grav-plugin-admin\/issues\/2394)\n   * Fixed a path traversal vulnerability with file uploads [#GHSA-m7hx-hw6h-mqmc](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-m7hx-hw6h-mqmc)\n   * Fixed a security issue with insecure Twig functions be processed [#GHSA-2m7x-c7px-hp58](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-2m7x-c7px-hp58) [#GHSA-r6vw-8v8r-pmp4](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-r6vw-8v8r-pmp4) [#GHSA-qfv4-q44r-g7rv](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-qfv4-q44r-g7rv) [#GHSA-c9gp-64c4-2rrh](https:\/\/github.com\/getgrav\/grav\/security\/advisories\/GHSA-c9gp-64c4-2rrh)\n1. [](#improved)\n   * Updated composer packages\n   * Updated `bin\/composer.phar` to latest `2.7.2`\n"},"1.7.44":{"date":"01\/05\/2024","content":"1. [](#new)\n   * Added PHP `8.3` to tests [#3782](https:\/\/github.com\/getgrav\/grav\/pull\/3782)\n   * Added debugger messages when Page routes conflict\n   * Added `ISO 8601` date format [#3721](https:\/\/github.com\/getgrav\/grav\/pull\/37210)\n   * Added support for `.vcf` (vCard) in media configuration [#3772](https:\/\/github.com\/getgrav\/grav\/pull\/3772)\n1. [](#improved)\n   * Update jQuery to `v3.6.4` [#3713](https:\/\/github.com\/getgrav\/grav\/pull\/3713)\n   * Updated vendor libraries including Dom-Sanitizer `v1.0.7` that addresses an XSS issue\n   * Updated `bin\/composer.phar` to latest `2.6.6`\n   * Updated vendor libraries to latest\n   * Updated language files\n   * Updated copyright year\n1. [](#bugfix)\n   * Fixed a math rounding issue with number validation when using floating point steps [#3761](https:\/\/github.com\/getgrav\/grav\/issues\/3761)\n   * Fixed an issue with `Inflector::ordinalize()` not working as expected [#3759](https:\/\/github.com\/getgrav\/grav\/pull\/3759)\n   * Fixed various issues with file extension checking with dangerous extensions [#3756(https:\/\/github.com\/getgrav\/grav\/pull\/3756)]\n   * Fix for invalid input to foreach in `UserGroupObject` [#3724](https:\/\/github.com\/getgrav\/grav\/pull\/3724)\n   * Fixed exception: `Property 'jsmodule_pipeline_include_externals' does not exist in object` [#3661](https:\/\/github.com\/getgrav\/grav\/pull\/3661)\n   * Fixed `too few arguments exception` in FlexObjects [#3658](https:\/\/github.com\/getgrav\/grav\/pull\/3658)\n"},"1.7.43":{"date":"10\/02\/2023","content":"1. [](#new)\n   * Add the ability to programatically set a page's `modified` timestamp via a `modified:` frontmatter entry\n2. [](#improved)\n   * Update vendor libraries\n   * Include `phar` in the list of `security.uploads_dangerous_extensions`\n   * When enabled `system.languages.debug` now dumps **Key -> Value** to debugger [#3752](https:\/\/github.com\/getgrav\/grav\/issues\/3752)\n   * Updated built-in composer to latest `2.6.4` [#3748](https:\/\/github.com\/getgrav\/grav\/issues\/3748)\n   * Added support for `@import` to ensure paths are rewritten correctly in CSS pipeline [#3750](https:\/\/github.com\/getgrav\/grav\/pull\/3750)\n"},"1.7.42.3":{"date":"07\/18\/2023","content":"2. [](#improved)\n   * Fixed a typo in `Utils::isDangerousFunction`\n"}}},"1.8":{"version":"1.8.0-beta.29","date":"2025-12-28T02:53:38Z","url":"https:\/\/github.com\/getgrav\/grav\/releases\/tag\/1.8.0-beta.29","min_php":"8.3.0","assets":{"grav-admin":{"name":"grav-admin-v1.8.0-beta.29.zip","type":"binary\/octet-stream","size":21077663,"download":"https:\/\/getgrav.org\/download\/core\/grav-admin\/1.8.0-beta.29?testing","content_sha256":"976765dcb40a8a5b69f9451641c2c9f062475481e89732bccc791a3134e46134","sha256":"328c0893edfee80bc40583b68f0a80a9efce95d69e0b6a3f7247dab743f29a40"},"grav-update":{"name":"grav-update-v1.8.0-beta.29.zip","type":"binary\/octet-stream","size":11321421,"download":"https:\/\/getgrav.org\/download\/core\/grav-update\/1.8.0-beta.29?testing","content_sha256":"899b9f7e8fc529894f7b0834752abcbc172b02d2da1fbaee8e2abc6239b7bdf4","sha256":"a7d883b16ff342cfd6de452720abfa259249c7e9bcbda51f873bee082b5bab9f"},"grav":{"name":"grav-v1.8.0-beta.29.zip","type":"binary\/octet-stream","size":13495934,"download":"https:\/\/getgrav.org\/download\/core\/grav\/1.8.0-beta.29?testing","content_sha256":"f5835471e975c4e2367cb7b1cede8add95dc6f51df68a8b4d9ca367250ce2680","sha256":"6a3b72b692df9b4df070a10626873f6ab6de02cd6ae5ad64d9d0d7e7180fe8b2"}},"changelog":{"1.8.0-beta.29":{"date":"12\/27\/2025","content":"1. [](#improved)\n    * Avoid mail in twig content trigger security error\n    * Don\u2019t do internal grav-based gzip, rely on webserver\n    * Updated vendor libs\n1. [](#bugfix)\n    * Fix for grav not picking up config + page changes\n    * Fix for unusual format SVGs\n    * Fix for nested config changes\n    * Fix for user editing causing `hashed_password` to be removed\n    * Fix of setEscaper move in Twig 3.9+\n    * Fix for broken symlinks\n"},"1.8.0-beta.28":{"date":"12\/08\/2025","content":"1. [](#new)\n    * Added `updates.recovery_mode` config option to enable\/disable recovery mode\n    * Added admin blueprint toggle for recovery mode setting\n1. [](#improved)\n    * Redesigned recovery mode screen with clearer messaging and modern UI\n    * Added collapsible stack trace details to recovery mode screen\n    * Added \"Clear Recovery Mode\" button that works without token authentication\n    * Added \"Disable Recovery Mode\" option to disable via config from recovery screen\n    * Added stack trace capture for exceptions in recovery context\n    * Added PHP version validation from package's `defines.php` during safe upgrade\n    * Added proxy methods to `Twig3CompatibilityLoader` for backwards compatibility with plugins that call loader methods directly (addPath, prependPath, getPaths, etc.)\n1. [](#bugfix)\n    * Fixed recovery mode image path for Grav installations in subdirectories\n    * Fixed backup restriction preventing backups on systems with Grav installed under `\/var\/www` - Fixes [#4002](https:\/\/github.com\/getgrav\/grav\/issues\/4002)\n    * Fixed XSS false positives for legitimate HTML tags containing 'on' (caption, button, section) - Fixes [grav-plugin-admin#2472](https:\/\/github.com\/getgrav\/grav-plugin-admin\/issues\/2472)\n"},"1.8.0-beta.27":{"date":"11\/30\/2025","content":"1. [](#improved)\n    * Hardened Twig sandbox with expanded blacklist blocking 150+ dangerous functions and attack patterns\n    * Added static regex caching in Security class for improved performance\n    * Added path traversal protection to backup root configuration\n    * Added validation for language codes to prevent regex injection DoS\n1. [](#bugfix)\n    * Fixed path traversal vulnerability in username during account creation\n    * Fixed username uniqueness bypass allowing duplicate accounts\n    * Fixed arbitrary file read via `read_file()` Twig function\n    * Fixed DoS via malformed cron expressions in scheduler\n    * Fixed password hash exposure to frontend via JSON serialization\n    * Fixed email disclosure in user edit page title\n    * Fixed XSS via `isindex` tag bypass (CVE-2023-31506)\n    * Fixed issue with FlexObjects caching [flex-objects#187](https:\/\/github.com\/trilbymedia\/grav-plugin-flex-objects\/issues\/187)\n"},"1.8.0-beta.26":{"date":"11\/29\/2025","content":"1. [](#improved)\n    * Improvements for JS minification and now pulls any broken JS out of pipeline\n    * Disallow xref\/xhref in SVGs\n    * Upgraded to recently released Symfony 7.4\n1. [](#bugfix)\n   * fix range requests for partial content in Utils::downloads() - Fixes [#3990](https:\/\/github.com\/getgrav\/grav-plugin-admin\/issues\/3990)\n"},"1.8.0-beta.25":{"date":"11\/22\/2025","content":"1. [](#bugfix)\n   * Fixed Twig version\n"},"1.8.0-beta.24":{"date":"11\/20\/2025","content":"1. [](#improved)\n    * More Twig3 compatibility fixes and tests\n    * Changed snapshot creationg to use copy instead of move for improved reliability\n    * Lazy load page optimization\n    * Regex caching optimization\n    * Gated Debugger `addEvent()` optimization\n    * Various SafeUpgrade performance optimizations\n    * Improved Twig Deferred block implementation\n1. [](#bugfix)\n    * Fix various Twig3 deprecated notices\n    * Fixed slow purge snapshot functionality and test\n"},"1.8.0-beta.23":{"date":"11\/14\/2025","content":"1. [](#improved)\n    * Refactored safe-upgrade from scratch with simplified 'install' step\n"},"1.8.0-beta.22":{"date":"11\/06\/2025","content":"1. [](#bugfix)\n    * Removed over zealous safety checks\n    * Removed .gitattributes which was causing some unintended issues\n"},"1.8.0-beta.21":{"date":"11\/05\/2025","content":"1. [](#improved)\n    * Exclude dev files from exports\n1. [](#bugfix)\n    * Ignore .github and .phan folders during self-upgrade\n    * Fixed path check in self-upgrade\n"},"1.8.0-beta.20":{"date":"11\/05\/2025","content":"1. [](#bugfix)\n    * Fixed an issue where non-upgradable root-level folders were snapshotted\n"},"1.8.0-beta.19":{"date":"11\/05\/2025","content":"1. [](#new)\n    * Added new `bin\/gpm preflight` command\n    * Added `--safe` and `--legacy` overrides for `bin\/gpm self-upgrade` command\n1. [](#improved)\n    * Improved JS assets pipeline handling to support different loading strategies\n    * Cache fallbacks for unsupported Cache drivers\n    * More safe-upgrade fixes around safe guarding `\/user\/` and maintaining permissions better\n1. [](#bugfix)\n   * Fixed a regex issue that corrupted safe-upgrade output\n"},"1.8.0-beta.18":{"date":"10\/31\/2025","content":"1. [](#improved)\n    * Replaced legacy Doctrine cache dependency with Symfony-backed provider while keeping compatibility layer\n    * More safe-upgrade improvements\n    "},"1.8.0-beta.17":{"date":"10\/23\/2025","content":"1. [](#improved)\n    * Reworked `Monolog3` ship for better compatibility\n    * Latest vendor libraries\n    * Don't crash if `getManifest()` is not available\n    "},"1.8.0-beta.16":{"date":"10\/20\/2025","content":"1. [](#improved)\n    * Set `bin\/*` binaries to `+x` permission when upgrading via CLI\n    * Improved Twig3 compatibility fixes\n"},"1.8.0-beta.15":{"date":"10\/19\/2025","content":"1. [](#improved)\n    * Safe handling of disabled plugins\n    * Move `recover.flag` into `user:\/\/data`\n"},"1.8.0-beta.14":{"date":"10\/18\/2025","content":"1. [](#improved)\n    * Implemented more robust snapshot management via the `bin\/restore` command\n"},"1.8.0-beta.13":{"date":"10\/17\/2025","content":"1. [](#improved)\n    * Refactored safe-upgrade check to use copy-based snapshot\/install\/restore system\n"},"1.8.0-beta.12":{"date":"10\/17\/2025","content":"1. [](#bugfix)\n    * new low-level routing for safe-upgrade check\n"},"1.8.0-beta.11":{"date":"10\/16\/2025","content":"1. [](#bugfix)\n    * Sync 1.7 changes to 1.8 branch\n"},"1.8.0-beta.10":{"date":"10\/16\/2025","content":"1. [](#bugfix)\n    * Fixed an issue with **safe upgrade** losing dot files\n"}}}}}}
