Downloads
Everything Grav
Download Grav
Grav Core is the base package with core functionality and a few essential starting pages. Grav Core + Admin also includes the Administration Panel plugin. Both are easy to get started with — check out our Basic Tutorial and Guide to the Administration Panel.
Latest stable release
Production-ready. The version we recommend for every new site and every upgrade of an existing one.
Get Started
1
Quick installation
- Download either the Grav Core or Grav Core + Admin2 plugin installation package.
- Extract the zip file into your webroot.
- Point your browser at your local webserver:
http://yoursite.com
2
How to install the Admin2 plugin
If you have not already installed the admin plugin, you can do so easily with GPM:
$
This will install the admin plugin plus its dependencies (api & login). After this is complete, point your browser to your Grav installation and you will be prompted to create a new admin user.
Changelog
v2.0.23
Latest
20 hours ago
-
composer.jsonnow declares thectypeandsessionextensions it has always used, and suggestsfileinfoandsimplexml. Installing withcomposer create-projecton a machine missing one of these no longer quietly walks back to a years-old release of Grav instead of failing #4273
-
- 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
- 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 - A
GRAV_CONFIG__override set totrueorfalsenow reaches the configuration as a real yes/no value instead of the word itself, so switching something off from a.envfile or the server environment actually switches it off. Thanks to @nerdyjan for the report and @AdilAzhariOmsan for the fix #4277 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 asUNKNOWN. 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- On those same hosts the
system.http_x_forwardedoptions forip,client_ipandcf_connecting_iphad 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
v2.0.22 4 days ago
-
- 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) }} - Blueprints can use a
mediafield type, which saves a picked file as its path and keeps a list of them when the field allows more than one - Page collections can now exclude one or more template types with
notOfType(), the counterpart to the existingofType()#3910 - 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
- The
-
- Twig in page content can no longer read the site's configuration through the
print_r,vardump,json_encode,yaml_encodeandstringfilters. The check that was meant to stop it had been asking whether the whole site was in sandbox mode, which Grav never does — it decides per template — so it had been letting everything through. Thanks to @Vectrain51 - The
|map,|filterand|reduceTwig filters now check for themselves whether the template calling them is sandboxed, rather than relying on Twig to work it out. Thanks to @DhiyaneshGeek - 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
- 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
- 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.tokenoption. Cookies and API tokens are no longer recorded in profiler data whatever thecensoredoption is set to - Building a URL is now faster, which adds up over the hundreds of asset and link URLs a single page render produces
- 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
bin/grav scheduler -rnow records the run against each job, the same as a scheduled run, so the next run knows what has already happened- 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
- Multiline fields no longer carry a length limit low enough to affect real writing. Set
max: 0on a field to remove the limit altogether
- Twig in page content can no longer read the site's configuration through the
-
- The
|reduceTwig filter now actually reduces. It was running the|mapcode by mistake and throwing away the starting value, so[1,2,3]|reduce((c, v) => c + v, 0)gave back a list instead of6. Thanks to @DhiyaneshGeek - A form field's
minlengthandmaxlengthare 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 - A field with a
stepset 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 - 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
- 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.jpgon a site installed at/subdir - 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 - 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
- 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
- The same failure writing
user/config/versions.yamlno longer stops the site either #3688 - 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
- 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 Note: sites whosesystem.session.namecontains capitals, underscores or a leading or trailing dash will get a slightly different cookie name after this update, which signs their users out once. - 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 tobin/grav - 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
- 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
- A scheduler job registered without a schedule of its own no longer causes an error when its next run time is worked out
bin/grav scheduler -jno longer fails on a site with jobs registered by a plugin, and-dno longer fails on a job that has never run- 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
- 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 - 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
- With
force_sslturned 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 - 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,decodingandfetchpriorityvalues, which happened on every site whether or not those settings had been changed. Thanks to @wakqasahmed for the fix #4264
- The
v2.0.21 2 weeks ago
-
- 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
- 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
v2.0.20 2 weeks ago
-
- 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.
- 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.
- 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.
- The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.
- 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.
-
- [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.
- [security] The
read_filecapability 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. - [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.
- [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.
- A damaged page cache file is now rebuilt from the original page instead of stopping the site with a server error #4239
- Images and links in page content now work when the file name contains a colon, such as a screenshot named after a timestamp #3933
- 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
- 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
- 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.
- Flex directory blueprints no longer lose the fields the Flex Objects plugin adds when something reads the directory early in a request #160
- 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 - Audio and video players generated by
media.html()no longer carry analtattribute, which isn't valid on those elements; any alternative text is kept as an accessible label instead, so the markup passes validation #3540
v2.0.19 3 weeks ago
-
- You can now tighten the Twig content sandbox below its built-in defaults with new
denied_*settings insecurity.yaml. - The "Twig in Content" report can show the effective sandbox policy, so you can see exactly what page content is allowed to do.
- You can now tighten the Twig content sandbox below its built-in defaults with new
-
- A theme or plugin that ships its own
.htaccesscan no longer switch off the protection on its own folder, which used to leave its configuration and template files downloadable #4236 - Twig in page content now renders on new installs by default, instead of appearing as raw text until the setting was turned on.
- The long Twig sandbox allowlists now ship built into Grav, so
security.yamlonly records your own additions and future security updates to the defaults reach every site. - Existing sites that had trimmed those allowlists to tighten them keep exactly that policy after upgrading, now recorded as explicit
denied_*entries. - Removed two rarely-used Twig sandbox switches (
loggingandadmin_hint); both behaviours are now always on.
- A theme or plugin that ships its own
-
- 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. - 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
- The content cross-site scripting check no longer objects to harmless
v2.0.18 3 weeks ago
-
- [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).
- [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).
- [security] The Twig
sortandfindfilters 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). - 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).
- 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 - 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
- Grav now works out who the site runs as without starting a shell, so that detail still appears when external commands are unavailable
- 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
-
- 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
v2.0.23
Latest
20 hours ago
-
composer.jsonnow declares thectypeandsessionextensions it has always used, and suggestsfileinfoandsimplexml. Installing withcomposer create-projecton a machine missing one of these no longer quietly walks back to a years-old release of Grav instead of failing #4273
-
- 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
- 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 - A
GRAV_CONFIG__override set totrueorfalsenow reaches the configuration as a real yes/no value instead of the word itself, so switching something off from a.envfile or the server environment actually switches it off. Thanks to @nerdyjan for the report and @AdilAzhariOmsan for the fix #4277 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 asUNKNOWN. 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- On those same hosts the
system.http_x_forwardedoptions forip,client_ipandcf_connecting_iphad 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
v2.0.22 4 days ago
-
- 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) }} - Blueprints can use a
mediafield type, which saves a picked file as its path and keeps a list of them when the field allows more than one - Page collections can now exclude one or more template types with
notOfType(), the counterpart to the existingofType()#3910 - 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
- The
-
- Twig in page content can no longer read the site's configuration through the
print_r,vardump,json_encode,yaml_encodeandstringfilters. The check that was meant to stop it had been asking whether the whole site was in sandbox mode, which Grav never does — it decides per template — so it had been letting everything through. Thanks to @Vectrain51 - The
|map,|filterand|reduceTwig filters now check for themselves whether the template calling them is sandboxed, rather than relying on Twig to work it out. Thanks to @DhiyaneshGeek - 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
- 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
- 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.tokenoption. Cookies and API tokens are no longer recorded in profiler data whatever thecensoredoption is set to - Building a URL is now faster, which adds up over the hundreds of asset and link URLs a single page render produces
- 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
bin/grav scheduler -rnow records the run against each job, the same as a scheduled run, so the next run knows what has already happened- 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
- Multiline fields no longer carry a length limit low enough to affect real writing. Set
max: 0on a field to remove the limit altogether
- Twig in page content can no longer read the site's configuration through the
-
- The
|reduceTwig filter now actually reduces. It was running the|mapcode by mistake and throwing away the starting value, so[1,2,3]|reduce((c, v) => c + v, 0)gave back a list instead of6. Thanks to @DhiyaneshGeek - A form field's
minlengthandmaxlengthare 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 - A field with a
stepset 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 - 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
- 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.jpgon a site installed at/subdir - 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 - 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
- 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
- The same failure writing
user/config/versions.yamlno longer stops the site either #3688 - 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
- 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 Note: sites whosesystem.session.namecontains capitals, underscores or a leading or trailing dash will get a slightly different cookie name after this update, which signs their users out once. - 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 tobin/grav - 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
- 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
- A scheduler job registered without a schedule of its own no longer causes an error when its next run time is worked out
bin/grav scheduler -jno longer fails on a site with jobs registered by a plugin, and-dno longer fails on a job that has never run- 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
- 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 - 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
- With
force_sslturned 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 - 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,decodingandfetchpriorityvalues, which happened on every site whether or not those settings had been changed. Thanks to @wakqasahmed for the fix #4264
- The
v2.0.21 2 weeks ago
-
- 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
- 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
v2.0.20 2 weeks ago
-
- 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.
- 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.
- 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.
- The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.
- 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.
-
- [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.
- [security] The
read_filecapability 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. - [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.
- [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.
- A damaged page cache file is now rebuilt from the original page instead of stopping the site with a server error #4239
- Images and links in page content now work when the file name contains a colon, such as a screenshot named after a timestamp #3933
- 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
- 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
- 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.
- Flex directory blueprints no longer lose the fields the Flex Objects plugin adds when something reads the directory early in a request #160
- 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 - Audio and video players generated by
media.html()no longer carry analtattribute, which isn't valid on those elements; any alternative text is kept as an accessible label instead, so the markup passes validation #3540
v2.0.19 3 weeks ago
-
- You can now tighten the Twig content sandbox below its built-in defaults with new
denied_*settings insecurity.yaml. - The "Twig in Content" report can show the effective sandbox policy, so you can see exactly what page content is allowed to do.
- You can now tighten the Twig content sandbox below its built-in defaults with new
-
- A theme or plugin that ships its own
.htaccesscan no longer switch off the protection on its own folder, which used to leave its configuration and template files downloadable #4236 - Twig in page content now renders on new installs by default, instead of appearing as raw text until the setting was turned on.
- The long Twig sandbox allowlists now ship built into Grav, so
security.yamlonly records your own additions and future security updates to the defaults reach every site. - Existing sites that had trimmed those allowlists to tighten them keep exactly that policy after upgrading, now recorded as explicit
denied_*entries. - Removed two rarely-used Twig sandbox switches (
loggingandadmin_hint); both behaviours are now always on.
- A theme or plugin that ships its own
-
- 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. - 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
- The content cross-site scripting check no longer objects to harmless
v2.0.18 3 weeks ago
-
- [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).
- [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).
- [security] The Twig
sortandfindfilters 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). - 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).
- 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 - 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
- Grav now works out who the site runs as without starting a shell, so that detail still appears when external commands are unavailable
- 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
-
- 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