Compare commits

..

9 Commits

Author SHA1 Message Date
Andy Miller
bcd93c321b try again
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 22:18:43 +00:00
Andy Miller
8bd711f6b1 fixes for versions
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 21:42:03 +00:00
Andy Miller
fa707eb7eb vendor updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 21:38:55 +00:00
Andy Miller
18d285ec36 Merge branch 'develop' of github.com:getgrav/grav into develop 2025-11-24 21:06:07 +00:00
Andy Miller
04c6bdf287 disallow xref/xhref in SVGs
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 21:05:53 +00:00
Andy Miller
48343d7714 fix range requests for partial content in Utils::downloads() - Fixes #3990
Signed-off-by: Andy Miller <rhuk@mac.com>

# Conflicts:
#	system/src/Grav/Common/Utils.php
2025-11-23 18:03:28 +00:00
Andy Miller
9c27496cc1 test fixes + major/minor plugin warnings
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-12 13:05:37 +00:00
Andy Miller
fd51d33d3f added configurable snapshot pruning amount
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-11 19:30:05 +00:00
Andy Miller
7304612d3a some installer fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-11 17:55:14 +00:00
11 changed files with 328 additions and 203 deletions

View File

@@ -26,6 +26,7 @@
"symfony/polyfill-php80": "^1.23",
"symfony/polyfill-php81": "^1.23",
"psr/simple-cache": "^1.0",
"psr/cache": "^1.0",
"psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/container": "~1.1.0",
@@ -55,7 +56,8 @@
"league/climate": "^3.6",
"miljar/php-exif": "^0.6",
"composer/ca-bundle": "^1.2",
"dragonmantank/cron-expression": "^3.3",
"dragonmantank/cron-expression": "~3.3.0",
"symfony/deprecation-contracts": "^2.2",
"willdurand/negotiation": "^3.0",
"itsgoingd/clockwork": "^5.0",
"symfony/http-client": "^4.4",
@@ -64,13 +66,15 @@
"multiavatar/multiavatar-php": "^1.0"
},
"require-dev": {
"behat/gherkin": "~4.10.0",
"codeception/codeception": "^4.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpunit/php-code-coverage": "~9.2",
"getgrav/markdowndocs": "^2.0",
"codeception/module-asserts": "^1.3",
"codeception/module-phpbrowser": "^1.0"
"codeception/module-phpbrowser": "^1.0",
"doctrine/instantiator": "^1.4"
},
"replace": {
"symfony/polyfill-php72": "*",
@@ -87,7 +91,10 @@
"ext-exif": "Needed to use exif data from images."
},
"config": {
"apcu-autoloader": true
"apcu-autoloader": true,
"audit": {
"block-insecure": false
}
},
"autoload": {
"psr-4": {

295
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8d681f74b0bd1f5099bb8fbf788ab3eb",
"content-hash": "2d55f03bde4cf99b4790e878792a9ce0",
"packages": [
{
"name": "composer/ca-bundle",
"version": "1.5.8",
"version": "1.5.9",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "719026bb30813accb68271fee7e39552a58e9f65"
"reference": "1905981ee626e6f852448b7aaa978f8666c5bc54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65",
"reference": "719026bb30813accb68271fee7e39552a58e9f65",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/1905981ee626e6f852448b7aaa978f8666c5bc54",
"reference": "1905981ee626e6f852448b7aaa978f8666c5bc54",
"shasum": ""
},
"require": {
@@ -64,7 +64,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
"source": "https://github.com/composer/ca-bundle/tree/1.5.8"
"source": "https://github.com/composer/ca-bundle/tree/1.5.9"
},
"funding": [
{
@@ -76,7 +76,7 @@
"type": "github"
}
],
"time": "2025-08-20T18:49:47+00:00"
"time": "2025-11-06T11:46:17+00:00"
},
{
"name": "composer/semver",
@@ -255,6 +255,7 @@
"type": "tidelift"
}
],
"abandoned": true,
"time": "2022-05-20T20:06:54+00:00"
},
{
@@ -377,16 +378,16 @@
},
{
"name": "donatj/phpuseragentparser",
"version": "v1.10.0",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/donatj/PhpUserAgent.git",
"reference": "3ba73057d2a4a275badb88b7708e91e159c40367"
"reference": "c98541c5198bb75564d7db4a8971773bc848361e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/3ba73057d2a4a275badb88b7708e91e159c40367",
"reference": "3ba73057d2a4a275badb88b7708e91e159c40367",
"url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/c98541c5198bb75564d7db4a8971773bc848361e",
"reference": "c98541c5198bb75564d7db4a8971773bc848361e",
"shasum": ""
},
"require": {
@@ -431,7 +432,7 @@
],
"support": {
"issues": "https://github.com/donatj/PhpUserAgent/issues",
"source": "https://github.com/donatj/PhpUserAgent/tree/v1.10.0"
"source": "https://github.com/donatj/PhpUserAgent/tree/v1.11.0"
},
"funding": [
{
@@ -447,20 +448,20 @@
"type": "ko_fi"
}
],
"time": "2024-10-30T15:45:03+00:00"
"time": "2025-09-10T21:58:40+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.4.0",
"version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "8c784d071debd117328803d86b2097615b457500"
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500",
"reference": "8c784d071debd117328803d86b2097615b457500",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"shasum": ""
},
"require": {
@@ -473,14 +474,10 @@
"require-dev": {
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
@@ -504,7 +501,7 @@
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0"
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3"
},
"funding": [
{
@@ -512,7 +509,7 @@
"type": "github"
}
],
"time": "2024-10-09T13:47:03+00:00"
"time": "2023-08-10T19:36:49+00:00"
},
{
"name": "erusev/parsedown",
@@ -903,16 +900,16 @@
},
{
"name": "itsgoingd/clockwork",
"version": "v5.3.4",
"version": "v5.3.5",
"source": {
"type": "git",
"url": "https://github.com/itsgoingd/clockwork.git",
"reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5"
"reference": "d928483e231f042dbff9258795cb17aadaebc7d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
"reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
"url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/d928483e231f042dbff9258795cb17aadaebc7d0",
"reference": "d928483e231f042dbff9258795cb17aadaebc7d0",
"shasum": ""
},
"require": {
@@ -967,7 +964,7 @@
],
"support": {
"issues": "https://github.com/itsgoingd/clockwork/issues",
"source": "https://github.com/itsgoingd/clockwork/tree/v5.3.4"
"source": "https://github.com/itsgoingd/clockwork/tree/v5.3.5"
},
"funding": [
{
@@ -975,7 +972,7 @@
"type": "github"
}
],
"time": "2025-02-09T15:57:21+00:00"
"time": "2025-09-14T15:34:49+00:00"
},
{
"name": "league/climate",
@@ -2090,16 +2087,16 @@
},
{
"name": "rhukster/dom-sanitizer",
"version": "1.0.7",
"version": "1.0.8",
"source": {
"type": "git",
"url": "https://github.com/rhukster/dom-sanitizer.git",
"reference": "c2a98f27ad742668b254282ccc5581871d0fb601"
"reference": "757e4d6ac03afe9afa4f97cbef453fc5c25f0729"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/c2a98f27ad742668b254282ccc5581871d0fb601",
"reference": "c2a98f27ad742668b254282ccc5581871d0fb601",
"url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/757e4d6ac03afe9afa4f97cbef453fc5c25f0729",
"reference": "757e4d6ac03afe9afa4f97cbef453fc5c25f0729",
"shasum": ""
},
"require": {
@@ -2129,9 +2126,9 @@
"description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+",
"support": {
"issues": "https://github.com/rhukster/dom-sanitizer/issues",
"source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.7"
"source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.8"
},
"time": "2023-11-06T16:46:48+00:00"
"time": "2024-04-15T08:48:55+00:00"
},
{
"name": "rockettheme/toolbox",
@@ -2426,6 +2423,73 @@
],
"time": "2022-07-20T09:59:04+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "2.5-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v4.4.44",
@@ -3391,28 +3455,28 @@
},
{
"name": "webmozart/assert",
"version": "1.11.0",
"version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^7.2 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
@@ -3443,9 +3507,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
"source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
"time": "2022-06-03T18:03:27+00:00"
"time": "2025-10-29T15:56:20+00:00"
},
{
"name": "willdurand/negotiation",
@@ -4548,16 +4612,11 @@
},
{
"name": "phpstan/phpstan",
"version": "1.12.28",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9"
},
"version": "1.12.32",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
"reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
"reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
"shasum": ""
},
"require": {
@@ -4602,7 +4661,7 @@
"type": "github"
}
],
"time": "2025-07-17T17:15:39+00:00"
"time": "2025-09-30T10:16:31+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -4972,16 +5031,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.25",
"version": "9.6.29",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7"
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7",
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"shasum": ""
},
"require": {
@@ -5006,7 +5065,7 @@
"sebastian/comparator": "^4.0.9",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.6",
"sebastian/exporter": "^4.0.8",
"sebastian/global-state": "^5.0.8",
"sebastian/object-enumerator": "^4.0.4",
"sebastian/resource-operations": "^3.0.4",
@@ -5055,7 +5114,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29"
},
"funding": [
{
@@ -5079,7 +5138,7 @@
"type": "tidelift"
}
],
"time": "2025-08-20T14:38:31+00:00"
"time": "2025-09-24T06:29:11+00:00"
},
{
"name": "psr/http-client",
@@ -5574,16 +5633,16 @@
},
{
"name": "sebastian/exporter",
"version": "4.0.6",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"shasum": ""
},
"require": {
@@ -5639,15 +5698,27 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
"type": "tidelift"
}
],
"time": "2024-03-02T06:33:00+00:00"
"time": "2025-09-24T06:03:27+00:00"
},
{
"name": "sebastian/global-state",
@@ -6270,73 +6341,6 @@
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "2.5-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/dom-crawler",
"version": "v5.4.48",
@@ -6477,16 +6481,16 @@
},
{
"name": "theseer/tokenizer",
"version": "1.2.3",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@@ -6515,7 +6519,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.3"
"source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -6523,7 +6527,7 @@
"type": "github"
}
],
"time": "2024-03-03T12:36:25+00:00"
"time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
@@ -6542,8 +6546,5 @@
"ext-gd": "*"
},
"platform-dev": {},
"platform-overrides": {
"php": "7.3.6"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -1614,6 +1614,15 @@ form:
validate:
type: bool
updates.safe_upgrade_snapshot_limit:
type: number
label: PLUGIN_ADMIN.SAFE_UPGRADE_SNAPSHOT_LIMIT
help: PLUGIN_ADMIN.SAFE_UPGRADE_SNAPSHOT_LIMIT_HELP
default: 5
validate:
type: int
min: 0
http_section:
type: section
title: PLUGIN_ADMIN.HTTP_SECTION

View File

@@ -205,6 +205,7 @@ gpm:
updates:
safe_upgrade: true # Enable guarded staging+rollback pipeline for Grav self-updates
safe_upgrade_snapshot_limit: 5 # Maximum number of safe-upgrade snapshots to retain (0 = unlimited)
http:
method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL

View File

@@ -124,3 +124,5 @@ PLUGIN_ADMIN:
UPDATES_SECTION: Updates
SAFE_UPGRADE: Safe self-upgrade
SAFE_UPGRADE_HELP: When enabled, Grav core updates use staged installation with automatic rollback support.
SAFE_UPGRADE_SNAPSHOT_LIMIT: Safe-upgrade snapshots to keep
SAFE_UPGRADE_SNAPSHOT_LIMIT_HELP: Maximum number of snapshots to retain for safe upgrades (0 disables pruning).

View File

@@ -51,6 +51,7 @@ class Security
{
if (Grav::instance()['config']->get('security.sanitize_svg')) {
$sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
$sanitizer->addDisallowedAttributes(['href', 'xlink:href']);
$sanitized = $sanitizer->sanitize($svg);
if (is_string($sanitized)) {
$svg = $sanitized;
@@ -70,6 +71,7 @@ class Security
{
if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) {
$sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
$sanitizer->addDisallowedAttributes(['href', 'xlink:href']);
$original_svg = file_get_contents($file);
$clean_svg = $sanitizer->sanitize($original_svg);

View File

@@ -168,13 +168,11 @@ class SafeUpgradeService
$psrLogConflicts = $this->detectPsrLogConflicts();
$monologConflicts = $this->detectMonologConflicts();
// Only enforce plugin updates for major/minor upgrades
// For patch upgrades, just warn but don't block
if ($pending) {
if ($isMajorMinorUpgrade) {
$warnings[] = 'One or more plugins/themes are not up to date and must be updated for major version upgrades.';
$warnings[] = 'Because this is a major Grav upgrade, update pending plugins and themes before continuing.';
} else {
$warnings[] = 'One or more plugins/themes are not up to date.';
$warnings[] = 'Pending plugin/theme updates detected. Update them before running Grav upgrade.';
}
}
if ($psrLogConflicts) {

View File

@@ -693,6 +693,17 @@ abstract class Utils
header('Content-Disposition: attachment; filename="' . ($options['download_name'] ?? $file_parts['basename']) . '"');
}
if ($grav['config']->get('system.cache.enabled')) {
$expires = $options['expires'] ?? $grav['config']->get('system.pages.expires');
if ($expires > 0) {
$expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
header('Cache-Control: max-age=' . $expires);
header('Expires: ' . $expires_date);
header('Pragma: cache');
}
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
}
// multipart-download and download resuming support
if (isset($_SERVER['HTTP_RANGE'])) {
[$a, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2);
@@ -705,7 +716,7 @@ abstract class Utils
$range_end = (int)$range_end;
}
$new_length = $range_end - $range + 1;
header('HTTP/1.1 206 Partial Content');
http_response_code(206);
header("Content-Length: {$new_length}");
header("Content-Range: bytes {$range}-{$range_end}/{$size}");
} else {
@@ -714,19 +725,10 @@ abstract class Utils
header('Content-Length: ' . $size);
if ($grav['config']->get('system.cache.enabled')) {
$expires = $options['expires'] ?? $grav['config']->get('system.pages.expires');
if ($expires > 0) {
$expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
header('Cache-Control: max-age=' . $expires);
header('Expires: ' . $expires_date);
header('Pragma: cache');
}
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
// Return 304 Not Modified if the file is already cached in the browser
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file)) {
header('HTTP/1.1 304 Not Modified');
strtotime((string) $_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file)) {
http_response_code(304);
exit();
}
}

View File

@@ -29,6 +29,7 @@ use function date;
use function count;
use function is_callable;
use function strlen;
use function stripos;
/**
* Class SelfupgradeCommand
@@ -396,6 +397,21 @@ class SelfupgradeCommand extends GpmCommand
$conflicts = $preflight['psr_log_conflicts'] ?? [];
$monologConflicts = $preflight['monolog_conflicts'] ?? [];
$warnings = $preflight['warnings'] ?? [];
$isMajorMinorUpgrade = $preflight['is_major_minor_upgrade'] ?? null;
if ($isMajorMinorUpgrade === null && $this->upgrader) {
$local = $this->upgrader->getLocalVersion();
$remote = $this->upgrader->getRemoteVersion();
$localParts = explode('.', $local);
$remoteParts = explode('.', $remote);
$localMajor = (int)($localParts[0] ?? 0);
$localMinor = (int)($localParts[1] ?? 0);
$remoteMajor = (int)($remoteParts[0] ?? 0);
$remoteMinor = (int)($remoteParts[1] ?? 0);
$isMajorMinorUpgrade = ($localMajor !== $remoteMajor) || ($localMinor !== $remoteMinor);
}
$isMajorMinorUpgrade = (bool)$isMajorMinorUpgrade;
if ($warnings) {
$io->newLine();
@@ -419,25 +435,7 @@ class SelfupgradeCommand extends GpmCommand
return true;
}
if ($pending) {
// Use the is_major_minor_upgrade flag from preflight result if available
$isMajorMinorUpgrade = $preflight['is_major_minor_upgrade'] ?? false;
// Fall back to calculating it if not provided (for backwards compatibility)
if (!isset($preflight['is_major_minor_upgrade']) && $this->upgrader) {
$local = $this->upgrader->getLocalVersion();
$remote = $this->upgrader->getRemoteVersion();
$localParts = explode('.', $local);
$remoteParts = explode('.', $remote);
$localMajor = (int)($localParts[0] ?? 0);
$localMinor = (int)($localParts[1] ?? 0);
$remoteMajor = (int)($remoteParts[0] ?? 0);
$remoteMinor = (int)($remoteParts[1] ?? 0);
$isMajorMinorUpgrade = ($localMajor !== $remoteMajor) || ($localMinor !== $remoteMinor);
}
if ($pending && $isMajorMinorUpgrade) {
$local = $this->upgrader ? $this->upgrader->getLocalVersion() : 'unknown';
$remote = $this->upgrader ? $this->upgrader->getRemoteVersion() : 'unknown';
@@ -450,15 +448,9 @@ class SelfupgradeCommand extends GpmCommand
$io->writeln(sprintf(' - %s (%s) %s → %s', $slug, $type, $current, $available));
}
if ($isMajorMinorUpgrade) {
// For major/minor upgrades, this is EXPECTED behavior - updating plugins first is REQUIRED
$io->writeln(' For major version upgrades (v' . $local . ' → v' . $remote . '), plugins must be updated to their latest');
$io->writeln(' compatible versions BEFORE upgrading Grav core to ensure compatibility.');
$io->writeln(' Please run `bin/gpm update` to update these packages, then retry self-upgrade.');
} else {
// For patch upgrades, this shouldn't normally happen but plugins still need updating
$io->writeln(' Please run `bin/gpm update` to bring these packages current before upgrading Grav.');
}
$io->writeln(' For major version upgrades (v' . $local . ' → v' . $remote . '), plugins must be updated to their latest');
$io->writeln(' compatible versions BEFORE upgrading Grav core to ensure compatibility.');
$io->writeln(' Please run `bin/gpm update` to update these packages, then retry self-upgrade.');
$proceed = false;
if (!$this->all_yes) {

View File

@@ -20,13 +20,16 @@ use Grav\Common\Plugins;
use Grav\Common\Yaml;
use RuntimeException;
use Throwable;
use function array_slice;
use function basename;
use function class_exists;
use function count;
use function date;
use function dirname;
use function explode;
use function floor;
use function function_exists;
use function file_get_contents;
use function glob;
use function iterator_to_array;
use function is_dir;
@@ -42,6 +45,7 @@ use function array_fill_keys;
use function array_map;
use function array_pad;
use function array_key_exists;
use function rsort;
use function sort;
use function sprintf;
use function strtolower;
@@ -161,6 +165,8 @@ final class Install
private static $forceSafeUpgrade = null;
/** @var bool */
private static $allowPendingOverride = false;
/** @var int|null */
private static $snapshotLimit = null;
/** @var callable|null */
private $progressCallback = null;
/** @var array|null */
@@ -430,6 +436,35 @@ ERR;
return false;
}
private function getSafeUpgradeSnapshotLimit(): int
{
if (null !== self::$snapshotLimit) {
return self::$snapshotLimit;
}
$limit = 5;
try {
$grav = Grav::instance();
if ($grav && isset($grav['config'])) {
$configured = $grav['config']->get('system.updates.safe_upgrade_snapshot_limit');
if ($configured !== null) {
$limit = (int)$configured;
}
}
} catch (\Throwable $e) {
// ignore bootstrap failures
}
if ($limit < 0) {
$limit = 0;
}
self::$snapshotLimit = $limit;
return $limit;
}
private function captureCoreSnapshot(string $targetVersion): ?array
{
$entries = $this->collectSnapshotEntries();
@@ -484,6 +519,7 @@ ERR;
$this->persistSnapshotManifest($manifest);
$this->lastManifest = $manifest;
$this->pruneOldSnapshots($snapshotRoot);
return $manifest;
}
@@ -596,6 +632,83 @@ ERR;
}
}
private function pruneOldSnapshots(?string $snapshotRoot): void
{
$limit = $this->getSafeUpgradeSnapshotLimit();
if ($limit <= 0) {
return;
}
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
$files = glob($manifestDir . '/*.json');
if (!$files) {
return;
}
rsort($files);
if (count($files) <= $limit) {
return;
}
$obsolete = array_slice($files, $limit);
$removed = 0;
foreach ($obsolete as $manifestPath) {
$manifest = null;
try {
$contents = @file_get_contents($manifestPath);
if ($contents !== false) {
$decoded = json_decode($contents, true);
if (is_array($decoded)) {
$manifest = $decoded;
}
}
} catch (\Throwable $e) {
// ignore malformed manifests
}
$snapshotId = $manifest['id'] ?? basename($manifestPath, '.json');
$backupPath = $manifest['backup_path'] ?? null;
if ($backupPath && is_dir($backupPath)) {
try {
Folder::delete($backupPath);
} catch (\Throwable $e) {
error_log('[Grav Upgrade] Unable to delete snapshot directory ' . $backupPath . ': ' . $e->getMessage());
}
} elseif ($snapshotRoot && $snapshotId) {
$candidate = $snapshotRoot . '/' . $snapshotId;
if (is_dir($candidate)) {
try {
Folder::delete($candidate);
} catch (\Throwable $e) {
error_log('[Grav Upgrade] Unable to delete snapshot directory ' . $candidate . ': ' . $e->getMessage());
}
}
}
if (!@unlink($manifestPath)) {
error_log('[Grav Upgrade] Unable to remove snapshot manifest: ' . $manifestPath);
continue;
}
$removed++;
}
if ($removed > 0) {
$this->relayProgress(
'snapshot',
sprintf(
'Pruned %d old snapshot%s (keeping latest %d).',
$removed,
$removed === 1 ? '' : 's',
$limit
),
null
);
}
}
/**
* @return void
@@ -604,7 +717,7 @@ ERR;
public function finalize(): void
{
$start = microtime(true);
$this->relayProgress('postflight', 'Running postflight tasks...', null);
$this->relayProgress('finalizing', 'Running postflight tasks...', null);
// Finalize can be run without installing Grav first.
if (null === $this->updater) {
$versions = Versions::instance(USER_DIR . 'config/versions.yaml');
@@ -624,7 +737,7 @@ ERR;
}
$elapsed = microtime(true) - $start;
$this->relayProgress('postflight', sprintf('Postflight tasks complete in %.3fs.', $elapsed), null);
$this->relayProgress('finalizing', sprintf('Postflight tasks complete in %.3fs.', $elapsed), null);
}
/**
@@ -777,7 +890,7 @@ ERR;
private function runPreflightChecks(string $targetVersion): array
{
$start = microtime(true);
$this->relayProgress('preflight', 'Running preflight checks...', null);
$this->relayProgress('initializing', 'Running preflight checks...', null);
$report = [
'warnings' => [],
'psr_log_conflicts' => [],
@@ -795,14 +908,12 @@ ERR;
if (self::$allowPendingOverride) {
$report['warnings'][] = 'Pending plugin/theme updates ignored for this upgrade run.';
} elseif ($report['is_major_minor_upgrade']) {
$report['blocking'][] = 'Plugin and theme updates must be applied before upgrading Grav.';
} else {
$report['warnings'][] = 'Pending plugin/theme updates detected; updating before upgrading Grav is recommended.';
$report['blocking'][] = 'Pending plugin/theme updates detected. Because this is a major Grav upgrade, update them before continuing.';
}
}
$elapsed = microtime(true) - $start;
$this->relayProgress('preflight', sprintf('Preflight checks complete in %.3fs.', $elapsed), null);
$this->relayProgress('initializing', sprintf('Preflight checks complete in %.3fs.', $elapsed), null);
return $report;
}
@@ -888,7 +999,7 @@ ERR;
}
}
$this->relayProgress('preflight', sprintf('Detected %d updatable packages (including symlinks).', count($pending)), null);
$this->relayProgress('initializing', sprintf('Detected %d updatable packages (including symlinks).', count($pending)), null);
return $pending;
}

View File

@@ -46,7 +46,7 @@ class SelfupgradeCommandTest extends \Codeception\TestCase\Test
public function testHandlePreflightReportAbortsOnPendingWhenDeclined(): void
{
$command = new TestSelfupgradeCommand();
[$style] = $this->injectIo($command);
[$style] = $this->injectIo($command, [false]);
$this->setAllYes($command, false);
$result = $command->runHandle([