mirror of
https://github.com/getgrav/grav.git
synced 2025-12-05 15:29:57 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58002f4903 | ||
|
|
19a9fafe37 | ||
|
|
8ad4c006a2 | ||
|
|
52fd9a6e7b | ||
|
|
45e6ed941f | ||
|
|
2c2b2fc2e4 | ||
|
|
b0301beee3 | ||
|
|
ce6a1b3bcb | ||
|
|
d42adcd593 | ||
|
|
bcd93c321b | ||
|
|
8bd711f6b1 | ||
|
|
fa707eb7eb | ||
|
|
18d285ec36 | ||
|
|
04c6bdf287 | ||
|
|
3ddc548d51 | ||
|
|
48343d7714 | ||
|
|
9c27496cc1 | ||
|
|
fd51d33d3f | ||
|
|
7304612d3a | ||
|
|
e6025670ea | ||
|
|
92b3d5b1f8 | ||
|
|
2ee3ff074c | ||
|
|
4fab5f99bb | ||
|
|
1d5d1357b8 | ||
|
|
eb649c35a3 | ||
|
|
9b75d96bbf | ||
|
|
41d771da7c |
12
.github/dependabot.yaml
vendored
Normal file
12
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
6
.github/workflows/build.yaml
vendored
6
.github/workflows/build.yaml
vendored
@@ -15,7 +15,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Extract Tag
|
||||
run: echo "PACKAGE_VERSION=${{ github.ref }}" >> $GITHUB_ENV
|
||||
@@ -65,7 +67,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: technote-space/workflow-conclusion-action@v2
|
||||
- uses: technote-space/workflow-conclusion-action@v3
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: failure
|
||||
|
||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP ${{ matrix.php }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
2
.github/workflows/trigger-skeletons.yml
vendored
2
.github/workflows/trigger-skeletons.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
WORKFLOW: "build-skeleton.yml"
|
||||
AUTH: ":${{secrets.GLOBAL_TOKEN}}"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- name: Make it rain ☔️
|
||||
run: |
|
||||
SKELETONS=`curl -s "${{secrets.SKELETONS_JSON_LIST}}"`
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -52,3 +52,4 @@ system/templates/testing/*
|
||||
tmp/*
|
||||
#needs_fixing.txt
|
||||
/AGENTS.md
|
||||
/.claude
|
||||
|
||||
96
.travis.yml
96
.travis.yml
@@ -1,96 +0,0 @@
|
||||
language: php
|
||||
php:
|
||||
- '7.1'
|
||||
- '7.2'
|
||||
- '7.3'
|
||||
- '7.4'
|
||||
branches:
|
||||
only:
|
||||
- build_test
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: always
|
||||
slack:
|
||||
secure: dowksPsxxCxGKT6nis5hUgkp6+ZDAhoqzQHF9rJnx4hx0iEygPhVBs7pKl9yL2jubYJoLs+EXwE7z1dYgDAEJh4BnfrCokCMLpFGcxVxQC/HeAUdSQ2/RtdBYR5PRT75ScaFpqM/SfXXZVtnwVXAw9Z+JC6BjQ9vmn23m51Jw4k=
|
||||
env:
|
||||
global:
|
||||
# Colors!
|
||||
- TEXTRESET=$(tput sgr0) # reset the foreground colour
|
||||
- RED=$(tput setaf 1)
|
||||
- GREEN=$(tput setaf 2)
|
||||
- YELLOW=$(tput setaf 3)
|
||||
- BLUE=$(tput setaf 4)
|
||||
- BOLD=$(tput bold)
|
||||
# User
|
||||
- GH_USER="getgrav"
|
||||
# Paths
|
||||
- RT_DEVTOOLS=$HOME/devtools
|
||||
- GOPATH="$HOME/go"
|
||||
- PATH="$GOPATH/bin:$PATH"
|
||||
# GH_TOKEN [API Key]
|
||||
- secure: "NR9pV7YteY9OoPmjDTQG0fDfocVu+tCeiDH1F2GFhXCu71UOIvqWXpOxp0RHkG5GIXdCFHx59yu+ZO275lbaHkbF8+4lVSVrV4RcGn+pIncvxr6iZCVW05dbAxV3H8alK+xYJRGmbyfQl5wIM49WvmuGHZjcmIloS4t/omQ3N+I="
|
||||
# BB_TOKEN value => "user:pass@"
|
||||
- secure: "einUtSEkUWy2IrqLXyVjwUU+mwaaoiOXRRVdLBpA3Zye6bZx8cm5h/5AplkPWhM/NmCJoW/MwNZHHkFhlr3mDRov5iOxVmTTYfnXB+I5lxYTSgduOLLErS7mU8hfADpVDU8bHNU44fNGD3UEiG1PD4qQBX4DMlqIFmR20mjs81k="
|
||||
# GH_API_USER [for curl]
|
||||
- secure: "AQGcX1B2NrI8ajflY4AimZDNcK2kBA3F6mbtEFQ78NkDoWhMipsQHayWXiSTzRc0YJKvQl2Y16MTwQF4VHzjTAiiZFATgA8J88vQUjIPabi/kKjqSmcLFoaAOAxStQbW6e0z2GiQ6KBMcNF1y5iUuI63xVrBvtKrYX/w5y+ako8="
|
||||
|
||||
before_install:
|
||||
- export TZ=Pacific/Honolulu
|
||||
- echo $TRAVIS_PHP_VERSION
|
||||
- echo $TRAVIS_BRANCH
|
||||
- echo $TRAVIS_PULL_REQUEST
|
||||
- composer self-update
|
||||
- if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then
|
||||
composer install --dev --prefer-dist;
|
||||
fi
|
||||
- |
|
||||
if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
|
||||
export TRAVIS_TAG=$(curl -H "Authorization: token ${GH_TOKEN}" --fail -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4);
|
||||
eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.13 bash)";
|
||||
go get github.com/github-release/github-release;
|
||||
git clone --quiet --depth=50 --branch=master https://${BB_TOKEN}bitbucket.org/rockettheme/grav-devtools.git $RT_DEVTOOLS &>/dev/null;
|
||||
if [ ! -z "$TRAVIS_TAG" ]; then
|
||||
cd ${RT_DEVTOOLS};
|
||||
./build-grav.sh skeletons.txt;
|
||||
fi;
|
||||
fi
|
||||
before_script:
|
||||
- phpenv config-rm xdebug.ini
|
||||
script:
|
||||
- if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then
|
||||
vendor/bin/codecept run;
|
||||
fi
|
||||
- echo "Latest Release Tag - ${TRAVIS_TAG}"
|
||||
- if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
|
||||
FILES="$RT_DEVTOOLS/grav-dist/*.zip";
|
||||
for file in ${FILES[@]}; do
|
||||
NAME=${file##*/};
|
||||
if [[ "$NAME" == *"-rc"* ]]; then
|
||||
REPO="$(echo ${NAME} | rev | cut -f 3- -d "-" | rev)";
|
||||
else
|
||||
REPO="$(echo ${NAME} | rev | cut -f 2- -d "-" | rev)";
|
||||
fi;
|
||||
if [[ $REPO == 'grav' || $REPO == 'grav-admin' || $REPO == 'grav-update' ]]; then
|
||||
REPO="grav";
|
||||
fi;
|
||||
API="$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/${GH_USER}/${REPO}/releases/latest)";
|
||||
ASSETS="$(echo "${API}" | node gh-assets.js)";
|
||||
TAG="$(echo "${API}" | grep tag_name | head -n 1 | cut -d '"' -f 4)";
|
||||
if [ $REPO == "grav" ]; then
|
||||
TAG="$TRAVIS_TAG";
|
||||
fi;
|
||||
if [ ! -z "$ASSETS" ]; then
|
||||
for asset in ${ASSETS[@]}; do
|
||||
asset_id=$(echo ${asset} | cut -d ':' -f 1);
|
||||
asset_name=$(echo ${asset} | cut -d ':' -f 2);
|
||||
if [ "${NAME}" == "${asset_name}" ]; then
|
||||
echo -e "\nAsset ${BOLD}${BLUE}${NAME}${TEXTRESET} already exists in ${YELLOW}${REPO}${TEXTRESET}@${BOLD}${YELLOW}${TAG}${TEXTRESET}... deleting id ${BOLD}${RED}${asset_id}${TEXTRESET}...";
|
||||
curl -X DELETE --fail --user "${GH_API_USER}" "https://api.github.com/repos/${GH_USER}/${REPO}/releases/assets/${asset_id}";
|
||||
fi;
|
||||
done;
|
||||
fi;
|
||||
echo "Uploading package ${BOLD}${BLUE}${NAME}${TEXTRESET} to ${YELLOW}${REPO}${TEXTRESET}@${YELLOW}${TAG}${TEXTRESET}";
|
||||
github-release upload --security-token $GH_TOKEN --user ${GH_USER} --repo $REPO --tag "$TAG" --name "$NAME" --file "$file";
|
||||
done;
|
||||
fi
|
||||
@@ -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
295
composer.lock
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,13 @@ if (!defined('GRAV_ROOT')) {
|
||||
// Check if Install class is already loaded (from an older Grav version)
|
||||
// This happens when upgrading from older versions where the OLD Install class
|
||||
// was loaded via autoloader before extracting the update package (e.g., via Install::forceSafeUpgrade())
|
||||
$logInstallerSource = static function ($install, string $source) {
|
||||
$sourceLabel = $source === 'extracted update package' ? 'update package' : 'existing installation';
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
echo sprintf(" |- Using installer from %s\n", $sourceLabel);
|
||||
}
|
||||
};
|
||||
|
||||
if (class_exists('Grav\\Installer\\Install', false)) {
|
||||
// OLD Install class is already loaded. We cannot load the NEW one due to PHP limitations.
|
||||
// However, we can work around this by:
|
||||
@@ -31,10 +38,15 @@ if (class_exists('Grav\\Installer\\Install', false)) {
|
||||
$locationProp->setValue($install, __DIR__ . '/..');
|
||||
}
|
||||
|
||||
$logInstallerSource($install, 'existing installation');
|
||||
|
||||
return $install;
|
||||
}
|
||||
|
||||
// Normal case: Install class not yet loaded, load the NEW one
|
||||
require_once __DIR__ . '/src/Grav/Installer/Install.php';
|
||||
|
||||
return Grav\Installer\Install::instance();
|
||||
$install = Grav\Installer\Install::instance();
|
||||
$logInstallerSource($install, 'extracted update package');
|
||||
|
||||
return $install;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
|
||||
new TwigFilter('wordcount', [$this, 'wordCountFilter']),
|
||||
new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']),
|
||||
new TwigFilter('array_unique', 'array_unique'),
|
||||
new TwigFilter('array_group_by', [$this, 'arrayGroupByFilter'], ['needs_environment' => true]),
|
||||
new TwigFilter('basename', 'basename'),
|
||||
new TwigFilter('dirname', 'dirname'),
|
||||
new TwigFilter('print_r', [$this, 'print_r']),
|
||||
@@ -192,6 +193,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
|
||||
new TwigFunction('array_key_exists', 'array_key_exists'),
|
||||
new TwigFunction('array_unique', 'array_unique'),
|
||||
new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']),
|
||||
new TwigFunction('array_group_by', [$this, 'arrayGroupByFilter'], ['needs_environment' => true]),
|
||||
new TwigFunction('array_diff', 'array_diff'),
|
||||
new TwigFunction('authorize', [$this, 'authorize']),
|
||||
new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
|
||||
@@ -1281,6 +1283,67 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
|
||||
return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group items in an array by the results of a callback function
|
||||
*
|
||||
* @param Environment $env The Twig environment
|
||||
* @param array|\Traversable $array The array or collection to group
|
||||
* @param string|callable $callback Property name or callable to determine group key
|
||||
* @return array Grouped array with keys as group identifiers and values as arrays of items
|
||||
*/
|
||||
public function arrayGroupByFilter(Environment $env, $array, $callback): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
// Convert to array if it's a Traversable object (like Grav Collections)
|
||||
if ($array instanceof \Traversable) {
|
||||
$array = iterator_to_array($array);
|
||||
}
|
||||
|
||||
if (!is_array($array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($array as $key => $item) {
|
||||
// If callback is a string, treat it as a property/method name
|
||||
if (is_string($callback)) {
|
||||
// Try to get the value using different methods
|
||||
if (is_array($item) && isset($item[$callback])) {
|
||||
$groupKey = $item[$callback];
|
||||
} elseif (is_object($item)) {
|
||||
if (method_exists($item, $callback)) {
|
||||
$groupKey = $item->$callback();
|
||||
} elseif (property_exists($item, $callback)) {
|
||||
$groupKey = $item->$callback;
|
||||
} elseif (method_exists($item, '__get')) {
|
||||
$groupKey = $item->$callback;
|
||||
} else {
|
||||
$groupKey = 'undefined';
|
||||
}
|
||||
} else {
|
||||
$groupKey = 'undefined';
|
||||
}
|
||||
} else {
|
||||
// Execute the callback function
|
||||
try {
|
||||
$groupKey = call_user_func($callback, $item, $key, $env);
|
||||
} catch (\Exception $e) {
|
||||
$groupKey = 'undefined';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize group array if it doesn't exist
|
||||
if (!isset($groups[$groupKey])) {
|
||||
$groups[$groupKey] = [];
|
||||
}
|
||||
|
||||
// Add item to its group
|
||||
$groups[$groupKey][] = $item;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve a cookie value
|
||||
*
|
||||
|
||||
@@ -18,29 +18,24 @@ use Grav\Common\Yaml;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use RecursiveCallbackFilterIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use FilesystemIterator;
|
||||
use function array_key_exists;
|
||||
use function basename;
|
||||
use function chgrp;
|
||||
use function chmod;
|
||||
use function chown;
|
||||
use function copy;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function filegroup;
|
||||
use function fileowner;
|
||||
use function glob;
|
||||
use function in_array;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function lchgrp;
|
||||
use function lchown;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function property_exists;
|
||||
@@ -54,8 +49,6 @@ use function trim;
|
||||
use function uniqid;
|
||||
use function unlink;
|
||||
use function ltrim;
|
||||
use function posix_getgrgid;
|
||||
use function posix_getpwuid;
|
||||
use const GRAV_ROOT;
|
||||
use const GLOB_ONLYDIR;
|
||||
use const JSON_PRETTY_PRINT;
|
||||
@@ -74,7 +67,7 @@ class SafeUpgradeService
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const IMPLEMENTATION_VERSION = '20251106'; // 2025-11-06 - Added preflight to Install.php
|
||||
public const IMPLEMENTATION_VERSION = '20251118'; // 2025-11-18 - Fixed snapshot creation (copy vs move) and implemented pruning
|
||||
|
||||
/** @var string */
|
||||
private $rootPath;
|
||||
@@ -100,8 +93,6 @@ class SafeUpgradeService
|
||||
];
|
||||
/** @var callable|null */
|
||||
private $progressCallback = null;
|
||||
/** @var int */
|
||||
private $metadataWarningCount = 0;
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
@@ -178,13 +169,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) {
|
||||
@@ -271,9 +260,11 @@ class SafeUpgradeService
|
||||
$this->reportProgress('installing', 'Copying update files...', null);
|
||||
|
||||
try {
|
||||
$this->copyEntries($packageEntries, $packagePath, $this->rootPath, 'installing', 'Deploying');
|
||||
$this->copyEntries($packageEntries, $packagePath, $this->rootPath, 'installing', 'Deploying', true);
|
||||
} catch (Throwable $e) {
|
||||
$this->copyEntries($snapshotEntries, $backupPath, $this->rootPath, 'installing', 'Restoring');
|
||||
// Rollback: restore from snapshot
|
||||
$this->reportProgress('rollback', 'Upgrade failed, restoring from snapshot...', null);
|
||||
$this->copyEntries($packageEntries, $backupPath, $this->rootPath, 'rollback', 'Restoring', false);
|
||||
throw new RuntimeException('Failed to promote staged Grav release.', 0, $e);
|
||||
}
|
||||
|
||||
@@ -341,6 +332,51 @@ class SafeUpgradeService
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a snapshot specifically for automated upgrades.
|
||||
*
|
||||
* @param string $targetVersion
|
||||
* @param string|null $label
|
||||
* @return array
|
||||
*/
|
||||
public function createUpgradeSnapshot(string $targetVersion, ?string $label = null): array
|
||||
{
|
||||
$entries = $this->collectPackageEntries($this->rootPath);
|
||||
if (!$entries) {
|
||||
throw new RuntimeException('Unable to locate files to snapshot.');
|
||||
}
|
||||
|
||||
$stageId = uniqid('upgrade-', false);
|
||||
$backupPath = $this->stagingRoot . DIRECTORY_SEPARATOR . 'snapshot-' . $stageId;
|
||||
|
||||
$this->reportProgress('snapshot', sprintf('Capturing snapshot before upgrading to %s...', $targetVersion), null, [
|
||||
'operation' => 'upgrade',
|
||||
'target_version' => $targetVersion,
|
||||
]);
|
||||
|
||||
$this->createBackupSnapshot($entries, $backupPath);
|
||||
|
||||
$manifest = $this->buildManifest($stageId, $targetVersion, $this->rootPath, $backupPath, $entries);
|
||||
$manifest['package_path'] = null;
|
||||
if ($label !== null && $label !== '') {
|
||||
$manifest['label'] = $label;
|
||||
}
|
||||
$manifest['operation'] = 'upgrade';
|
||||
$manifest['mode'] = 'pre-upgrade';
|
||||
|
||||
$this->persistManifest($manifest);
|
||||
$this->lastManifest = $manifest;
|
||||
$this->pruneOldSnapshots();
|
||||
|
||||
$this->reportProgress('snapshot', sprintf('Snapshot %s captured.', $stageId), 100, [
|
||||
'operation' => 'upgrade',
|
||||
'snapshot' => $stageId,
|
||||
'target_version' => $targetVersion,
|
||||
]);
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
private function collectPackageEntries(string $packagePath): array
|
||||
{
|
||||
$entries = [];
|
||||
@@ -381,7 +417,7 @@ class SafeUpgradeService
|
||||
|
||||
Folder::create($packagePath);
|
||||
$entries = $this->collectPackageEntries($sourcePath);
|
||||
$this->copyEntries($entries, $sourcePath, $packagePath, 'installing', 'Staging');
|
||||
$this->copyEntries($entries, $sourcePath, $packagePath, 'installing', 'Staging', true);
|
||||
Folder::delete($sourcePath);
|
||||
|
||||
return 'copy';
|
||||
@@ -390,10 +426,10 @@ class SafeUpgradeService
|
||||
private function createBackupSnapshot(array $entries, string $backupPath): void
|
||||
{
|
||||
Folder::create($backupPath);
|
||||
$this->copyEntries($entries, $this->rootPath, $backupPath, 'snapshot', 'Snapshotting');
|
||||
$this->copyEntries($entries, $this->rootPath, $backupPath, 'snapshot', 'Snapshotting', false);
|
||||
}
|
||||
|
||||
private function copyEntries(array $entries, string $sourceBase, string $targetBase, ?string $progressStage = null, ?string $progressPrefix = null): void
|
||||
private function copyEntries(array $entries, string $sourceBase, string $targetBase, ?string $progressStage = null, ?string $progressPrefix = null, bool $useMove = false): void
|
||||
{
|
||||
$total = count($entries);
|
||||
foreach ($entries as $index => $entry) {
|
||||
@@ -428,7 +464,9 @@ class SafeUpgradeService
|
||||
}
|
||||
|
||||
$destination = $targetBase . DIRECTORY_SEPARATOR . $entry;
|
||||
$metadata = $this->captureEntryMeta($destination);
|
||||
|
||||
// Use the same simple approach as traditional upgrade:
|
||||
// Delete old, copy new, let filesystem handle ownership
|
||||
$this->removeEntry($destination);
|
||||
|
||||
if (is_link($source)) {
|
||||
@@ -436,29 +474,42 @@ class SafeUpgradeService
|
||||
if (!@symlink(readlink($source), $destination)) {
|
||||
throw new RuntimeException(sprintf('Failed to replicate symlink "%s".', $source));
|
||||
}
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
continue;
|
||||
} elseif (is_dir($source)) {
|
||||
Folder::create(dirname($destination));
|
||||
Folder::rcopy($source, $destination, true);
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
continue;
|
||||
|
||||
if ($useMove) {
|
||||
// Use move() like traditional upgrade - faster than copy
|
||||
Folder::move($source, $destination);
|
||||
} else {
|
||||
Folder::rcopy($source, $destination, true);
|
||||
}
|
||||
|
||||
// Set bin/ permissions like traditional upgrade does
|
||||
if ($entry === 'bin') {
|
||||
$binFiles = glob($destination . DIRECTORY_SEPARATOR . '*') ?: [];
|
||||
foreach ($binFiles as $binFile) {
|
||||
@chmod($binFile, 0755);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Folder::create(dirname($destination));
|
||||
if (!@copy($source, $destination)) {
|
||||
throw new RuntimeException(sprintf('Failed to copy file "%s" to "%s".', $source, $destination));
|
||||
}
|
||||
$perm = @fileperms($source);
|
||||
if ($perm !== false) {
|
||||
@chmod($destination, $perm & 0777);
|
||||
}
|
||||
$mtime = @filemtime($source);
|
||||
if ($mtime !== false) {
|
||||
@touch($destination, $mtime);
|
||||
if ($useMove) {
|
||||
if (!@rename($source, $destination)) {
|
||||
if (!@copy($source, $destination)) {
|
||||
throw new RuntimeException(sprintf('Failed to move file "%s" to "%s".', $source, $destination));
|
||||
}
|
||||
@unlink($source);
|
||||
}
|
||||
} else {
|
||||
if (!@copy($source, $destination)) {
|
||||
throw new RuntimeException(sprintf('Failed to copy file "%s" to "%s".', $source, $destination));
|
||||
}
|
||||
$perms = @fileperms($source);
|
||||
if ($perms !== false) {
|
||||
@chmod($destination, $perms & 0777);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,172 +522,6 @@ class SafeUpgradeService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture ownership and permission data for an existing filesystem entry.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function captureEntryMeta(string $path): array
|
||||
{
|
||||
if (!file_exists($path) && !is_link($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'link' => is_link($path),
|
||||
];
|
||||
|
||||
$perms = @fileperms($path);
|
||||
if ($perms !== false) {
|
||||
$meta['perms'] = $perms & 0777;
|
||||
}
|
||||
|
||||
$owner = @fileowner($path);
|
||||
if ($owner !== false) {
|
||||
$meta['owner'] = $owner;
|
||||
}
|
||||
|
||||
$group = @filegroup($path);
|
||||
if ($group !== false) {
|
||||
$meta['group'] = $group;
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply ownership and permission data to a copied entry when possible.
|
||||
*
|
||||
* @param string $path
|
||||
* @param array<string, mixed> $meta
|
||||
* @return void
|
||||
*/
|
||||
private function applyEntryMeta(string $path, array $meta): void
|
||||
{
|
||||
if (!$meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($meta['perms'])) {
|
||||
$result = @chmod($path, (int) $meta['perms']);
|
||||
if ($result === false) {
|
||||
$this->logMetadataWarning('chmod', $path, $meta['perms']);
|
||||
}
|
||||
}
|
||||
|
||||
$isLink = !empty($meta['link']);
|
||||
|
||||
if (isset($meta['owner'])) {
|
||||
$owner = $this->resolveOwner($meta['owner']);
|
||||
$result = false;
|
||||
if ($isLink && function_exists('lchown')) {
|
||||
$result = @lchown($path, $owner);
|
||||
} elseif (!$isLink && function_exists('chown')) {
|
||||
$result = @chown($path, $owner);
|
||||
}
|
||||
if ($result === false) {
|
||||
$this->logMetadataWarning('chown', $path, $owner);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($meta['group'])) {
|
||||
$group = $this->resolveGroup($meta['group']);
|
||||
$result = false;
|
||||
if ($isLink && function_exists('lchgrp')) {
|
||||
$result = @lchgrp($path, $group);
|
||||
} elseif (!$isLink && function_exists('chgrp')) {
|
||||
$result = @chgrp($path, $group);
|
||||
}
|
||||
if ($result === false) {
|
||||
$this->logMetadataWarning('chgrp', $path, $group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning when metadata operations fail.
|
||||
*
|
||||
* @param string $operation Operation that failed (chmod, chown, chgrp)
|
||||
* @param string $path Path to the file/directory
|
||||
* @param mixed $value Value that was attempted (permissions, owner, group)
|
||||
* @return void
|
||||
*/
|
||||
private function logMetadataWarning(string $operation, string $path, $value): void
|
||||
{
|
||||
$this->metadataWarningCount++;
|
||||
|
||||
// Try to get Grav logger if available
|
||||
try {
|
||||
$grav = Grav::instance();
|
||||
if (isset($grav['log'])) {
|
||||
$grav['log']->warning(sprintf(
|
||||
'Safe-upgrade: Failed to apply %s(%s, %s). File permissions/ownership may not be preserved correctly. ' .
|
||||
'This is usually not critical but may require manual permission fixes after upgrade.',
|
||||
$operation,
|
||||
$path,
|
||||
is_scalar($value) ? $value : gettype($value)
|
||||
));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently continue if logging fails - don't break the upgrade
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of metadata warnings during upgrade.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMetadataWarningCount(): int
|
||||
{
|
||||
return $this->metadataWarningCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stored owner identifier to a format accepted by chown/lchown.
|
||||
*
|
||||
* @param int|string $owner
|
||||
* @return int|string
|
||||
*/
|
||||
private function resolveOwner($owner)
|
||||
{
|
||||
if (is_string($owner)) {
|
||||
return $owner;
|
||||
}
|
||||
|
||||
if (function_exists('posix_getpwuid')) {
|
||||
$info = @posix_getpwuid((int) $owner);
|
||||
if (is_array($info) && isset($info['name'])) {
|
||||
return $info['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return (int) $owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stored group identifier to a format accepted by chgrp/lchgrp.
|
||||
*
|
||||
* @param int|string $group
|
||||
* @return int|string
|
||||
*/
|
||||
private function resolveGroup($group)
|
||||
{
|
||||
if (is_string($group)) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
if (function_exists('posix_getgrgid')) {
|
||||
$info = @posix_getgrgid((int) $group);
|
||||
if (is_array($info) && isset($info['name'])) {
|
||||
return $info['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return (int) $group;
|
||||
}
|
||||
|
||||
public function setProgressCallback(?callable $callback): self
|
||||
{
|
||||
$this->progressCallback = $callback;
|
||||
@@ -678,7 +563,7 @@ class SafeUpgradeService
|
||||
}
|
||||
|
||||
$this->reportProgress('rollback', 'Restoring snapshot...', null);
|
||||
$this->copyEntries($entries, $backupPath, $this->rootPath, 'rollback', 'Restoring');
|
||||
$this->copyEntries($entries, $backupPath, $this->rootPath, 'rollback', 'Restoring', false);
|
||||
$this->markRollback($manifest['id']);
|
||||
$this->lastManifest = $manifest;
|
||||
|
||||
@@ -903,16 +788,24 @@ class SafeUpgradeService
|
||||
continue;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
$directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
|
||||
$filter = new RecursiveCallbackFilterIterator($directory, static function ($current, $key, $iterator) {
|
||||
// Skip hidden files/dirs (starting with .)
|
||||
if ($current->getFilename()[0] === '.') {
|
||||
return false;
|
||||
}
|
||||
if ($iterator->hasChildren()) {
|
||||
// Exclude vendor and node_modules directories
|
||||
return !in_array($current->getFilename(), ['vendor', 'node_modules'], true);
|
||||
}
|
||||
// Only include PHP files
|
||||
return $current->getExtension() === 'php';
|
||||
});
|
||||
|
||||
$iterator = new RecursiveIteratorIterator($filter);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
/** @var \SplFileInfo $file */
|
||||
if (!$file->isFile() || strtolower($file->getExtension()) !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = @file_get_contents($file->getPathname());
|
||||
if ($contents === false) {
|
||||
continue;
|
||||
@@ -1252,14 +1145,75 @@ class SafeUpgradeService
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only the three newest snapshots.
|
||||
* Keep only the newest snapshots based on configuration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function pruneOldSnapshots(): void
|
||||
{
|
||||
// Retain all snapshots; administrators can prune manually if desired.
|
||||
// Legacy behaviour removed to ensure full history remains available.
|
||||
return;
|
||||
$limit = 5;
|
||||
|
||||
if ($this->config) {
|
||||
$limit = (int)$this->config->get('system.updates.safe_upgrade_snapshot_limit', 5);
|
||||
} else {
|
||||
try {
|
||||
$grav = Grav::instance();
|
||||
if (isset($grav['config'])) {
|
||||
$limit = (int)$grav['config']->get('system.updates.safe_upgrade_snapshot_limit', 5);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
if ($limit <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($this->manifestStore . DIRECTORY_SEPARATOR . '*.json') ?: [];
|
||||
if (count($files) <= $limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
$manifests = [];
|
||||
foreach ($files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
$data = json_decode($content, true);
|
||||
if (is_array($data) && isset($data['created_at'])) {
|
||||
$manifests[] = [
|
||||
'path' => $file,
|
||||
'created_at' => $data['created_at'],
|
||||
'backup_path' => $data['backup_path'] ?? null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by created_at descending
|
||||
usort($manifests, static function ($a, $b) {
|
||||
$result = $b['created_at'] <=> $a['created_at'];
|
||||
if ($result === 0) {
|
||||
return strcmp($b['path'], $a['path']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
|
||||
$toDelete = array_slice($manifests, $limit);
|
||||
|
||||
foreach ($toDelete as $item) {
|
||||
// Delete manifest
|
||||
@unlink($item['path']);
|
||||
|
||||
// Delete backup directory if it exists
|
||||
if ($item['backup_path'] && is_dir($item['backup_path'])) {
|
||||
// Ensure we are deleting a directory inside staging root to be safe
|
||||
if (strpos($item['backup_path'], $this->stagingRoot) === 0) {
|
||||
Folder::delete($item['backup_path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use function date;
|
||||
use function count;
|
||||
use function is_callable;
|
||||
use function strlen;
|
||||
use function stripos;
|
||||
|
||||
/**
|
||||
* Class SelfupgradeCommand
|
||||
@@ -213,7 +214,7 @@ class SelfupgradeCommand extends GpmCommand
|
||||
Install::instance()->finalize();
|
||||
|
||||
$io->write(' |- Running post-install scripts... ');
|
||||
$io->writeln(" '- <green>Success!</green> ");
|
||||
$io->writeln(" |- <green>Success!</green> ");
|
||||
$io->newLine();
|
||||
}
|
||||
}
|
||||
@@ -292,11 +293,11 @@ class SelfupgradeCommand extends GpmCommand
|
||||
|
||||
$error = 0;
|
||||
if (!$installation) {
|
||||
$io->writeln(" '- <red>Installation failed or aborted.</red>");
|
||||
$io->writeln(" |- <red>Installation failed or aborted.</red>");
|
||||
$io->newLine();
|
||||
$error = 1;
|
||||
} else {
|
||||
$io->writeln(" '- <green>Success!</green> ");
|
||||
$io->writeln(" |- <green>Success!</green> ");
|
||||
|
||||
$manifest = Install::instance()->getLastManifest();
|
||||
if (is_array($manifest) && ($manifest['id'] ?? null)) {
|
||||
@@ -392,13 +393,25 @@ class SelfupgradeCommand extends GpmCommand
|
||||
{
|
||||
$io = $this->getIO();
|
||||
$pending = $preflight['plugins_pending'] ?? [];
|
||||
$blocking = $preflight['blocking'] ?? [];
|
||||
$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);
|
||||
|
||||
if (empty($pending) && empty($conflicts) && empty($monologConflicts)) {
|
||||
return true;
|
||||
$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();
|
||||
@@ -408,25 +421,21 @@ class SelfupgradeCommand extends GpmCommand
|
||||
}
|
||||
}
|
||||
|
||||
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 ($blocking && empty($pending)) {
|
||||
$io->newLine();
|
||||
$io->writeln('<red>Upgrade blocked:</red>');
|
||||
foreach ($blocking as $reason) {
|
||||
$io->writeln(' - ' . $reason);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($pending) && empty($conflicts) && empty($monologConflicts)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($pending && $isMajorMinorUpgrade) {
|
||||
$local = $this->upgrader ? $this->upgrader->getLocalVersion() : 'unknown';
|
||||
$remote = $this->upgrader ? $this->upgrader->getRemoteVersion() : 'unknown';
|
||||
|
||||
@@ -439,19 +448,24 @@ 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) {
|
||||
$question = new ConfirmationQuestion('Proceed anyway? [y|N] ', false);
|
||||
$proceed = $io->askQuestion($question);
|
||||
}
|
||||
|
||||
$io->writeln('Aborting self-upgrade. Run `bin/gpm update` first.');
|
||||
if (!$proceed) {
|
||||
$io->writeln('Aborting self-upgrade. Run `bin/gpm update` first.');
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
Install::allowPendingPackageOverride(true);
|
||||
$io->writeln(' › Proceeding despite pending plugin/theme updates.');
|
||||
}
|
||||
|
||||
$handled = $this->handleConflicts(
|
||||
@@ -643,6 +657,14 @@ class SelfupgradeCommand extends GpmCommand
|
||||
$this->handleServiceProgress($stage, $message, $percent);
|
||||
});
|
||||
}
|
||||
if (is_object($install) && method_exists($install, 'generatePreflightReport')) {
|
||||
$report = $install->generatePreflightReport();
|
||||
if (!$this->handlePreflightReport($report)) {
|
||||
Installer::setError('Upgrade aborted due to preflight requirements.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
$install($zip);
|
||||
} else {
|
||||
throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,7 +114,7 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
|
||||
self::assertDirectoryExists($manifest['backup_path']);
|
||||
}
|
||||
|
||||
public function testKeepsAllSnapshots(): void
|
||||
public function testPrunesOldSnapshots(): void
|
||||
{
|
||||
[$root, $manifestStore] = $this->prepareLiveEnvironment();
|
||||
$service = new SafeUpgradeService([
|
||||
@@ -123,7 +123,7 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
|
||||
]);
|
||||
|
||||
$manifests = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$package = $this->preparePackage((string)$i);
|
||||
$manifests[] = $service->promote($package, '1.8.' . $i, ['backup', 'cache', 'images', 'logs', 'tmp', 'user']);
|
||||
// Ensure subsequent promotions have a marker to restore.
|
||||
@@ -131,8 +131,17 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
|
||||
}
|
||||
|
||||
$files = glob($manifestStore . '/*.json');
|
||||
self::assertCount(4, $files);
|
||||
self::assertTrue(is_dir($manifests[0]['backup_path']));
|
||||
self::assertCount(5, $files);
|
||||
|
||||
// Verify the oldest one (index 0) is gone
|
||||
$oldestManifestId = $manifests[0]['id'];
|
||||
self::assertFileDoesNotExist($manifestStore . '/' . $oldestManifestId . '.json');
|
||||
self::assertDirectoryDoesNotExist($manifests[0]['backup_path']);
|
||||
|
||||
// Verify the newest one (index 5) exists
|
||||
$newestManifestId = $manifests[5]['id'];
|
||||
self::assertFileExists($manifestStore . '/' . $newestManifestId . '.json');
|
||||
self::assertDirectoryExists($manifests[5]['backup_path']);
|
||||
}
|
||||
|
||||
public function testDetectsPsrLogConflictsFromFilesystem(): void
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user