Skip to content

Commit 812e57d

Browse files
committed
Merge branch 'release/2.0.20'
2 parents 25f37b9 + d0857df commit 812e57d

39 files changed

Lines changed: 1003 additions & 52 deletions

.github/workflows/trigger-docker.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ jobs:
2525
steps:
2626
- name: Trigger docker-grav build
2727
run: |
28-
curl -X POST \
28+
# --fail-with-body so an expired GLOBAL_TOKEN stops this job instead of
29+
# letting it report success: plain curl treats a 401 as a completed
30+
# request, so the dispatch silently never reaches docker-grav.
31+
curl -X POST --fail-with-body --silent --show-error \
2932
-H "Accept: application/vnd.github+json" \
3033
-H "Authorization: Bearer ${{ secrets.GLOBAL_TOKEN }}" \
3134
-H "X-GitHub-Api-Version: 2022-11-28" \

.github/workflows/trigger-skeletons.yml

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,47 @@ jobs:
2525
- uses: actions/checkout@v7
2626
- name: Make it rain ☔️
2727
run: |
28-
SKELETONS=`curl -s "${{secrets.SKELETONS_JSON_LIST}}"`
29-
echo "$SKELETONS" | jq -cr '.[]' | while read SKELETON; do
28+
SKELETONS=$(curl --fail-with-body --silent --show-error "${{secrets.SKELETONS_JSON_LIST}}")
29+
if ! echo "$SKELETONS" | jq -e 'type == "array" and length > 0' > /dev/null; then
30+
echo "::error::Skeleton list is empty or is not a JSON array — dispatching nothing"
31+
exit 1
32+
fi
33+
34+
failed=0
35+
36+
# Fed by process substitution rather than a pipe: a `while` loop on the
37+
# right-hand side of a pipe runs in a subshell, so the failure count
38+
# would be discarded when the loop ends.
39+
while read -r SKELETON; do
3040
KEY=$(echo "$SKELETON" | jq -cr 'keys[0]')
3141
VERSION=$(echo "$SKELETON" | jq -cr '.[]')
3242
URL="https://api.github.com/repos/${KEY}/actions/workflows/${WORKFLOW}/dispatches"
3343
34-
curl -X POST \
35-
-u "${AUTH}" \
36-
-H "Accept: application/vnd.github.everest-preview+json" \
37-
-H "Content-Type: application/json" \
38-
-sS \
39-
${URL} \
40-
--data '{ "ref": "develop",
41-
"inputs": {
42-
"tag": "'"$VERSION"'",
43-
"version": "'"$INPUT_VERSION"'",
44-
"admin": "'"$INPUT_ADMIN"'"
45-
}
46-
}' > /dev/null
47-
echo "Dispatched Worfklow for ${KEY}@$VERSION"
48-
done
44+
# One rejected repo must not hide the two dozen that follow it, so a
45+
# failure is recorded and the loop carries on; the step fails at the
46+
# end with a count. --fail-with-body is what makes a rejection visible
47+
# at all: plain curl treats a 401 as a completed request, so a dead
48+
# GLOBAL_TOKEN used to report every skeleton as dispatched.
49+
if curl -X POST --fail-with-body --silent --show-error \
50+
-u "${AUTH}" \
51+
-H "Accept: application/vnd.github.everest-preview+json" \
52+
-H "Content-Type: application/json" \
53+
"${URL}" \
54+
--data '{ "ref": "develop",
55+
"inputs": {
56+
"tag": "'"$VERSION"'",
57+
"version": "'"$INPUT_VERSION"'",
58+
"admin": "'"$INPUT_ADMIN"'"
59+
}
60+
}'; then
61+
echo "Dispatched workflow for ${KEY}@${VERSION}"
62+
else
63+
echo "::error::Dispatch failed for ${KEY}@${VERSION}"
64+
failed=$((failed + 1))
65+
fi
66+
done < <(echo "$SKELETONS" | jq -cr '.[]')
67+
68+
if [ "$failed" -ne 0 ]; then
69+
echo "::error::${failed} skeleton dispatch(es) failed"
70+
exit 1
71+
fi

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
# v2.0.20
2+
## 08/21/2026
3+
4+
1. [](#improved)
5+
* 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.
6+
* 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.
7+
* 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.
8+
* The bundled Nginx configuration now sets caching headers for images, fonts, stylesheets and scripts, so visitors stop re-downloading them on every page.
9+
* 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.
10+
1. [](#bugfix)
11+
* [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.
12+
* [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.
13+
* [security] A proxy address that carries a username and password is now hidden from sandboxed page content, matching the other credentials already redacted there.
14+
* [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.
15+
* 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)
16+
* 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)
17+
* 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)
18+
* 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)
19+
* 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.
20+
* 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)
21+
* 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)
22+
* 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)
23+
124
# v2.0.19
225
## 08/14/2026
326

composer.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

system/config/security.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,15 @@ read_file:
183183
# Streams that read_file may resolve. Defaults cover content-reading needs
184184
# (active theme, all themes, page tree, user data) without exposing
185185
# accounts, configs, logs, cache, vendor, system, etc. Add more carefully.
186+
# `user-data` is deliberately absent: user://data is application state (form
187+
# submissions, scheduler history, notifications), not content, and a page
188+
# editor is not granted read access to it. A site that genuinely keeps
189+
# includable content there can add it back, accepting that everything else
190+
# under user://data becomes readable at the same time.
186191
allowed_streams:
187192
- theme
188193
- themes
189194
- page
190-
- user-data
191195
# File extensions read_file will read. Restricted to text/content formats
192196
# by design — adding `php`, `yaml`, `env`, `htaccess`, etc. here is a
193197
# foot-gun and not recommended.

system/config/system.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ assets: # Configuration for Assets Mana
160160
js_module_pipeline_include_externals: true # Include external URLs in the pipeline by default
161161
js_module_pipeline_before_excludes: true # Render the pipeline before any excluded files
162162
js_minify: true # Minify the JS during pipelining
163-
enable_asset_timestamp: false # Enable asset timestamps
163+
enable_asset_timestamp: false # Enable asset timestamps (local non-pipelined assets use their own file mtime)
164164
enable_asset_sri: false # Enable asset SRI
165165
collections:
166166
jquery: system://assets/jquery/jquery-3.x.min.js

system/defines.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
// Some standard defines
1111
define("GRAV", true);
12-
define("GRAV_VERSION", "2.0.19");
12+
define("GRAV_VERSION", "2.0.20");
1313
define("GRAV_SCHEMA", "1.8.0_2026-06-09_0");
1414
define("GRAV_TESTING", false);
1515

system/src/Grav/Common/Assets/BaseAsset.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,33 @@ public function init($asset, $options)
144144
$asset = $this->buildLocalLink($file->getPathname());
145145

146146
$this->modified = $file->isFile() ? $file->getMTime() : false;
147+
148+
// Rewrite the cache-busting token to this asset's own filemtime
149+
// instead of the global config/version-derived cache key, so
150+
// editing one file invalidates only that file's URL.
151+
//
152+
// Only ever replaces the token the flag itself generated — the
153+
// global cache key. An explicit, caller-supplied token set via the
154+
// public Assets::setTimestamp() API wins outright, whether or not
155+
// the config flag is on: pinning one release token across a fleet
156+
// is exactly what that API is for, and a config flag should not be
157+
// able to silently override an imperative call with no way out.
158+
//
159+
// Note: the rendered <link>/<script> tag for a *pipelined* bundle
160+
// still uses the global cache key (Pipeline::$timestamp, set from
161+
// Assets::render()), so this has no effect on pipelined output
162+
// URLs. It does still feed into the pipeline's internal bundle
163+
// uid hash (Pipeline::renderCss()/renderJs() hash the serialized
164+
// BaseAsset objects, which include $timestamp) — harmless, but
165+
// it does mean upgrading can produce one new bundle file instead
166+
// of reusing the previous one.
167+
if ($this->timestamp
168+
&& $this->modified
169+
&& $config->get('system.assets.enable_asset_timestamp')
170+
&& $this->timestamp === Grav::instance()['cache']->getKey()
171+
) {
172+
$this->timestamp = dechex($this->modified);
173+
}
147174
}
148175
}
149176

system/src/Grav/Common/Assets/Css.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ public function render()
4747
return "<style>\n" . trim($buffer) . "\n</style>\n";
4848
}
4949

50-
return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . ">\n";
50+
return '<link href="' . $this->escapeAssetUrl(trim($this->asset) . $this->renderQueryString()) . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . ">\n";
5151
}
5252
}

system/src/Grav/Common/Assets/Js.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ public function render()
4343
return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
4444
}
4545

46-
return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . "></script>\n";
46+
return '<script src="' . $this->escapeAssetUrl(trim($this->asset) . $this->renderQueryString()) . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . "></script>\n";
4747
}
4848
}

0 commit comments

Comments
 (0)