mirror of
https://github.com/getgrav/grav.git
synced 2025-12-16 05:09:42 +01:00
Compare commits
293 Commits
1.8.0-beta
...
15cb068f95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15cb068f95 | ||
|
|
d34213232b | ||
|
|
7a6b8a90d4 | ||
|
|
306f33f4ae | ||
|
|
6cb8229806 | ||
|
|
95e285efa4 | ||
|
|
80410dae13 | ||
|
|
fae70e5fc9 | ||
|
|
9d9247a32f | ||
|
|
94d85cd873 | ||
|
|
0f879bd1d4 | ||
|
|
fd828d452e | ||
|
|
63bbc1cac6 | ||
|
|
528032b11a | ||
|
|
a4c3a3af6d | ||
|
|
b7e1958a6e | ||
|
|
0c38968c58 | ||
|
|
9d11094e41 | ||
|
|
ed640a1314 | ||
|
|
e37259527d | ||
|
|
3462d94d57 | ||
|
|
19c2f8da76 | ||
|
|
a161399c84 | ||
|
|
5f120c328b | ||
|
|
db924c4a26 | ||
|
|
9fc1b42d59 | ||
|
|
c8878dfc80 | ||
|
|
779661ab8a | ||
|
|
3985638a8f | ||
|
|
a78789b291 | ||
|
|
caa127cd53 | ||
|
|
5f087d3a43 | ||
|
|
1bc6e5e13a | ||
|
|
f339bb83c5 | ||
|
|
27789991ae | ||
|
|
114aebae7c | ||
|
|
370dfd6016 | ||
|
|
1d05e6bdc4 | ||
|
|
3acff8a9f8 | ||
|
|
ea59bdb1d4 | ||
|
|
02330b96d9 | ||
|
|
2b1d73fd26 | ||
|
|
4e11ca7c8e | ||
|
|
591e2e4563 | ||
|
|
2161ffeb5e | ||
|
|
b856978211 | ||
|
|
19ee2d883e | ||
|
|
93089241c3 | ||
|
|
3b1c332932 | ||
|
|
7fd614f8b6 | ||
|
|
5567a5a1cd | ||
|
|
334e1dcabc | ||
|
|
cbf5ec57c6 | ||
|
|
9f33e247cf | ||
|
|
8c7e970603 | ||
|
|
360b418c97 | ||
|
|
af0db0c2a1 | ||
|
|
4c74192191 | ||
|
|
ee5fccd2c8 | ||
|
|
5bc89bf32b | ||
|
|
0b021e2114 | ||
|
|
15c1b1cc06 | ||
|
|
ee1b55e929 | ||
|
|
73d3a90c0b | ||
|
|
0764e37c8b | ||
|
|
bd5b2633f7 | ||
|
|
6b0c0486aa | ||
|
|
07ac3d3bb9 | ||
|
|
72e9d57e2e | ||
|
|
07965c6c61 | ||
|
|
72cc8e91a2 | ||
|
|
678eacaae5 | ||
|
|
cb7a3ccfdf | ||
|
|
076c10d34b | ||
|
|
2d75649a08 | ||
|
|
c8acc9a499 | ||
|
|
af499184ea | ||
|
|
ebac0a082c | ||
|
|
4d31bbb43a | ||
|
|
be20cf2e2c | ||
|
|
c33a1f57bc | ||
|
|
83817428c7 | ||
|
|
d2970a92b5 | ||
|
|
7b1bcf7789 | ||
|
|
44bdd1283d | ||
|
|
32dafbb1cb | ||
|
|
e622326285 | ||
|
|
d0287043c2 | ||
|
|
6c5b801c6f | ||
|
|
460bf241a5 | ||
|
|
ee179e19e5 | ||
|
|
3618a129df | ||
|
|
787146cc2c | ||
|
|
a1fe19f465 | ||
|
|
f2c26c116a | ||
|
|
d1d70c4d0c | ||
|
|
e5a659d445 | ||
|
|
39c4ecfe6a | ||
|
|
3e3aa00a1b | ||
|
|
9c2497460b | ||
|
|
f2f58d11d6 | ||
|
|
2d8be2f859 | ||
|
|
f6c57a44de | ||
|
|
0d2d0bdc11 | ||
|
|
e110701079 | ||
|
|
c10acd1837 | ||
|
|
f9f3b9a8ba | ||
|
|
e5b7449483 | ||
|
|
7077b0b71a | ||
|
|
57a446862f | ||
|
|
b2f2e7bd45 | ||
|
|
3fbd6771e9 | ||
|
|
8a10d6bc54 | ||
|
|
0bdde9dec2 | ||
|
|
348fa04c47 | ||
|
|
52f0d5f1d7 | ||
|
|
9c6111c368 | ||
|
|
9806533f56 | ||
|
|
e30245789c | ||
|
|
20b95c4585 | ||
|
|
6d0fc78462 | ||
|
|
5420ca2200 | ||
|
|
942f523f18 | ||
|
|
c812def317 | ||
|
|
9b2d352f8a | ||
|
|
d932875e66 | ||
|
|
7a2c151a4b | ||
|
|
81b0f0ec04 | ||
|
|
70ddb549b7 | ||
|
|
be3cb77f28 | ||
|
|
345b5e9577 | ||
|
|
e88f38bd10 | ||
|
|
bdc06afea2 | ||
|
|
f9348a4d9d | ||
|
|
44fd1172b8 | ||
|
|
c9c1267284 | ||
|
|
4fa5996414 | ||
|
|
920642411c | ||
|
|
2999c06a3a | ||
|
|
d97b2d70bd | ||
|
|
5e7b482972 | ||
|
|
9230a5a40f | ||
|
|
c3d1d4ae26 | ||
|
|
286b5a5179 | ||
|
|
c79d2ecfc4 | ||
|
|
70d6aec1a7 | ||
|
|
60a97dcf56 | ||
|
|
679a6db61d | ||
|
|
b70ae844a8 | ||
|
|
9dd507b717 | ||
|
|
e6de9db77e | ||
|
|
b6a37cfff3 | ||
|
|
42e37c1d02 | ||
|
|
09aa2fb8fd | ||
|
|
e764d2ce1c | ||
|
|
3f0b204728 | ||
|
|
f711cb3208 | ||
|
|
f10894fe47 | ||
|
|
b68872e3fd | ||
|
|
43126b09e4 | ||
|
|
6751d28839 | ||
|
|
8118d6b980 | ||
|
|
2c4b69f9ec | ||
|
|
d6cbc263e7 | ||
|
|
ba2536136b | ||
|
|
c56d24c0d7 | ||
|
|
ee49305053 | ||
|
|
b4d664fcb0 | ||
|
|
7192cfe549 | ||
|
|
7fcb1d1cb7 | ||
|
|
dbeaa8ad46 | ||
|
|
a3da588829 | ||
|
|
a3387c106b | ||
|
|
d9d241d806 | ||
|
|
bb5cdad333 | ||
|
|
44f90cbce0 | ||
|
|
a5c6f1dbe9 | ||
|
|
c8227b38fc | ||
|
|
77114ecdd0 | ||
|
|
23da92d0ff | ||
|
|
f88c09adca | ||
|
|
7dd5c8a0ba | ||
|
|
cf2ac28be2 | ||
|
|
43ddf45057 | ||
|
|
57212ec9a5 | ||
|
|
b55e86a8ba | ||
|
|
2b1a7d3fb6 | ||
|
|
250568bae5 | ||
|
|
cc97e2ff45 | ||
|
|
d92c430b8a | ||
|
|
184cdea75d | ||
|
|
7b9567ec28 | ||
|
|
75d8356f1b | ||
|
|
c82645a42a | ||
|
|
9e84d5d004 | ||
|
|
fd0d3dc463 | ||
|
|
eb985e875d | ||
|
|
ba3493adce | ||
|
|
d785042a0d | ||
|
|
49096b61f3 | ||
|
|
70e986074c | ||
|
|
4af22edd36 | ||
|
|
5bc7d6943f | ||
|
|
8eb4085bcd | ||
|
|
b47758e3c7 | ||
|
|
972ec26035 | ||
|
|
9116079e97 | ||
|
|
51ddb3984c | ||
|
|
22de638e52 | ||
|
|
365ab93e7e | ||
|
|
c172964025 | ||
|
|
cb0bbcdb8b | ||
|
|
35f5d2f329 | ||
|
|
03849923d4 | ||
|
|
3664096550 | ||
|
|
6664d98de8 | ||
|
|
17323ec76c | ||
|
|
9b9079bdd7 | ||
|
|
db3df738e8 | ||
|
|
38075f9c86 | ||
|
|
b0dac5f4b4 | ||
|
|
f7c77d1173 | ||
|
|
0fd734c8df | ||
|
|
b642b2c999 | ||
|
|
ae147fa53b | ||
|
|
9e6df39bff | ||
|
|
8ac6076f88 | ||
|
|
cef7812472 | ||
|
|
6f461395a7 | ||
|
|
162fe1acc2 | ||
|
|
332748a1f9 | ||
|
|
1a9a60115d | ||
|
|
a55052b8b0 | ||
|
|
dc7354e7e1 | ||
|
|
afc1513aad | ||
|
|
466b2a16e8 | ||
|
|
a21640ace6 | ||
|
|
09920deabc | ||
|
|
0bd72f4bb9 | ||
|
|
ec55a80183 | ||
|
|
d07ac5b6ea | ||
|
|
fa29f6672a | ||
|
|
006d8c85a0 | ||
|
|
c608ed10cf | ||
|
|
9d71de8e54 | ||
|
|
89764a51fb | ||
|
|
b851d9bf9d | ||
|
|
a0679fc050 | ||
|
|
e497a93da6 | ||
|
|
56cc894c1d | ||
|
|
d07f3770bc | ||
|
|
3baaf19c31 | ||
|
|
7236862a15 | ||
|
|
639be5ac0d | ||
|
|
8811b7aad0 | ||
|
|
4a22f3dc8d | ||
|
|
d3e32799ab | ||
|
|
3bebfc6dac | ||
|
|
9d80a4d992 | ||
|
|
fc7f72f89d | ||
|
|
fa56984dc3 | ||
|
|
90fd68d4a5 | ||
|
|
7613e38b6d | ||
|
|
830a442faa | ||
|
|
8d7b658aa6 | ||
|
|
83d098b891 | ||
|
|
d798859acd | ||
|
|
53391466b0 | ||
|
|
2fadc14c01 | ||
|
|
08d74df6e3 | ||
|
|
de2af9e470 | ||
|
|
edfb1a0868 | ||
|
|
9893a605a6 | ||
|
|
2620e836d4 | ||
|
|
83d291c24b | ||
|
|
7e723eb7f5 | ||
|
|
1d1d8da431 | ||
|
|
ca1b05ba57 | ||
|
|
4097d85daa | ||
|
|
350e4c04cd | ||
|
|
d8123a3662 | ||
|
|
1e430f635e | ||
|
|
17548131d3 | ||
|
|
4a27bd780c | ||
|
|
2e975dfa90 | ||
|
|
5666d0f211 | ||
|
|
d17ab9e06c | ||
|
|
a15fe29f43 | ||
|
|
3126fa8388 | ||
|
|
b0c339c9eb | ||
|
|
f812ee8555 | ||
|
|
a1e583f657 | ||
|
|
8c6388bb74 |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Extract Tag
|
||||
run: echo "PACKAGE_VERSION=${{ github.ref }}" >> $GITHUB_ENV
|
||||
@@ -23,7 +25,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
php-version: 8.3
|
||||
extensions: opcache, gd
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
32
.github/workflows/tests.yaml
vendored
32
.github/workflows/tests.yaml
vendored
@@ -2,28 +2,26 @@ name: PHP Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
branches: [ develop, 1.8 ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
branches: [ develop, 1.8 ]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
|
||||
unit-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
php: [8.5, 8.4, 8.3]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: [8.3, 8.2, 8.1, 8.0, 7.4, 7.3]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
- name: Setup PHP ${{ matrix.php }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -31,20 +29,14 @@ jobs:
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
env:
|
||||
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Update composer
|
||||
# run: composer update
|
||||
#
|
||||
# - name: Validate composer.json and composer.lock
|
||||
# run: composer validate
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -54,7 +46,7 @@ jobs:
|
||||
run: composer install --prefer-dist --no-progress
|
||||
|
||||
- name: Run test suite
|
||||
run: vendor/bin/codecept run
|
||||
run: php -d register_argc_argv=On vendor/bin/codecept run
|
||||
|
||||
# slack:
|
||||
# name: Slack
|
||||
|
||||
2
.github/workflows/trigger-skeletons.yml
vendored
2
.github/workflows/trigger-skeletons.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
admin:
|
||||
description: 'Create also a package with Admin'
|
||||
required: true
|
||||
default: true
|
||||
default: 'true'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,3 +48,7 @@ tests/cache/*
|
||||
tests/error.log
|
||||
system/templates/testing/*
|
||||
/user/config/versions.yaml
|
||||
/user/data/recovery.window
|
||||
tmp/*
|
||||
/AGENTS.md
|
||||
/.claude
|
||||
|
||||
290
CHANGELOG.md
290
CHANGELOG.md
@@ -1,3 +1,230 @@
|
||||
# v1.8.0-beta.28
|
||||
## 12/08/2025
|
||||
|
||||
1. [](#new)
|
||||
* Added `updates.recovery_mode` config option to enable/disable recovery mode
|
||||
* Added admin blueprint toggle for recovery mode setting
|
||||
1. [](#improved)
|
||||
* Redesigned recovery mode screen with clearer messaging and modern UI
|
||||
* Added collapsible stack trace details to recovery mode screen
|
||||
* Added "Clear Recovery Mode" button that works without token authentication
|
||||
* Added "Disable Recovery Mode" option to disable via config from recovery screen
|
||||
* Added stack trace capture for exceptions in recovery context
|
||||
* Added PHP version validation from package's `defines.php` during safe upgrade
|
||||
* Added proxy methods to `Twig3CompatibilityLoader` for backwards compatibility with plugins that call loader methods directly (addPath, prependPath, getPaths, etc.)
|
||||
1. [](#bugfix)
|
||||
* Fixed recovery mode image path for Grav installations in subdirectories
|
||||
* Fixed backup restriction preventing backups on systems with Grav installed under `/var/www` - Fixes [#4002](https://github.com/getgrav/grav/issues/4002)
|
||||
* Fixed XSS false positives for legitimate HTML tags containing 'on' (caption, button, section) - Fixes [grav-plugin-admin#2472](https://github.com/getgrav/grav-plugin-admin/issues/2472)
|
||||
|
||||
# v1.8.0-beta.27
|
||||
## 11/30/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Hardened Twig sandbox with expanded blacklist blocking 150+ dangerous functions and attack patterns
|
||||
* Added static regex caching in Security class for improved performance
|
||||
* Added path traversal protection to backup root configuration
|
||||
* Added validation for language codes to prevent regex injection DoS
|
||||
1. [](#bugfix)
|
||||
* Fixed path traversal vulnerability in username during account creation
|
||||
* Fixed username uniqueness bypass allowing duplicate accounts
|
||||
* Fixed arbitrary file read via `read_file()` Twig function
|
||||
* Fixed DoS via malformed cron expressions in scheduler
|
||||
* Fixed password hash exposure to frontend via JSON serialization
|
||||
* Fixed email disclosure in user edit page title
|
||||
* Fixed XSS via `isindex` tag bypass (CVE-2023-31506)
|
||||
* Fixed issue with FlexObjects caching [flex-objects#187](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/187)
|
||||
|
||||
# v1.8.0-beta.26
|
||||
## 11/29/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Improvements for JS minification and now pulls any broken JS out of pipeline
|
||||
* Disallow xref/xhref in SVGs
|
||||
* Upgraded to recently released Symfony 7.4
|
||||
1. [](#bugfix)
|
||||
* fix range requests for partial content in Utils::downloads() - Fixes [#3990](https://github.com/getgrav/grav-plugin-admin/issues/3990)
|
||||
|
||||
# v1.8.0-beta.25
|
||||
## 11/22/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed Twig version
|
||||
|
||||
# v1.8.0-beta.24
|
||||
## 11/20/2025
|
||||
|
||||
1. [](#improved)
|
||||
* More Twig3 compatibility fixes and tests
|
||||
* Changed snapshot creationg to use copy instead of move for improved reliability
|
||||
* Lazy load page optimization
|
||||
* Regex caching optimization
|
||||
* Gated Debugger `addEvent()` optimization
|
||||
* Various SafeUpgrade performance optimizations
|
||||
* Improved Twig Deferred block implementation
|
||||
1. [](#bugfix)
|
||||
* Fix various Twig3 deprecated notices
|
||||
* Fixed slow purge snapshot functionality and test
|
||||
|
||||
# v1.8.0-beta.23
|
||||
## 11/14/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Refactored safe-upgrade from scratch with simplified 'install' step
|
||||
|
||||
# v1.8.0-beta.22
|
||||
## 11/06/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Removed over zealous safety checks
|
||||
* Removed .gitattributes which was causing some unintended issues
|
||||
|
||||
# v1.8.0-beta.21
|
||||
## 11/05/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Exclude dev files from exports
|
||||
1. [](#bugfix)
|
||||
* Ignore .github and .phan folders during self-upgrade
|
||||
* Fixed path check in self-upgrade
|
||||
|
||||
# v1.8.0-beta.20
|
||||
## 11/05/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed an issue where non-upgradable root-level folders were snapshotted
|
||||
|
||||
# v1.8.0-beta.19
|
||||
## 11/05/2025
|
||||
|
||||
1. [](#new)
|
||||
* Added new `bin/gpm preflight` command
|
||||
* Added `--safe` and `--legacy` overrides for `bin/gpm self-upgrade` command
|
||||
1. [](#improved)
|
||||
* Improved JS assets pipeline handling to support different loading strategies
|
||||
* Cache fallbacks for unsupported Cache drivers
|
||||
* More safe-upgrade fixes around safe guarding `/user/` and maintaining permissions better
|
||||
1. [](#bugfix)
|
||||
* Fixed a regex issue that corrupted safe-upgrade output
|
||||
|
||||
# v1.8.0-beta.18
|
||||
## 10/31/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Replaced legacy Doctrine cache dependency with Symfony-backed provider while keeping compatibility layer
|
||||
* More safe-upgrade improvements
|
||||
|
||||
# v1.8.0-beta.17
|
||||
## 10/23/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Reworked `Monolog3` ship for better compatibility
|
||||
* Latest vendor libraries
|
||||
* Don't crash if `getManifest()` is not available
|
||||
|
||||
# v1.8.0-beta.16
|
||||
## 10/20/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Set `bin/*` binaries to `+x` permission when upgrading via CLI
|
||||
* Improved Twig3 compatibility fixes
|
||||
|
||||
# v1.8.0-beta.15
|
||||
## 10/19/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Safe handling of disabled plugins
|
||||
* Move `recover.flag` into `user://data`
|
||||
|
||||
# v1.8.0-beta.14
|
||||
## 10/18/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Implemented more robust snapshot management via the `bin/restore` command
|
||||
|
||||
# v1.8.0-beta.13
|
||||
## 10/17/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Refactored safe-upgrade check to use copy-based snapshot/install/restore system
|
||||
|
||||
# v1.8.0-beta.12
|
||||
## 10/17/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* new low-level routing for safe-upgrade check
|
||||
|
||||
# v1.8.0-beta.11
|
||||
## 10/16/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Sync 1.7 changes to 1.8 branch
|
||||
|
||||
# v1.8.0-beta.10
|
||||
## 10/16/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed an issue with **safe upgrade** losing dot files
|
||||
|
||||
# v1.8.0-beta.9
|
||||
## 10/16/2025
|
||||
|
||||
1. [](#new)
|
||||
* Added new **core safe upgrade** installer with staging, validation, and rollback support
|
||||
|
||||
# v1.8.0-beta.8
|
||||
## 10/14/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Upgraded to latest Symfony 7 (might cause issues with some plugins)
|
||||
* `wordCount` twig filter (merged from 1.7 branch)
|
||||
* More PHP 8.4 compatibility fixes
|
||||
* Update all vendor libraries to latest
|
||||
1. [](#bugfix)
|
||||
* Fixed some CLI level bugs
|
||||
* Fixed a Twig Sandbox bybpass issue
|
||||
|
||||
# v1.8.0-beta.7
|
||||
## 09/22/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Changed `private` to `public` for YamlUpdater::get() and YamUpdater::set() methods
|
||||
* Fixed a session cookie issue that manifested when logging-in to client side
|
||||
|
||||
# v1.8.0-beta.6
|
||||
## 09/22/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed a missing YamlUpdater::exists() method
|
||||
|
||||
# v1.8.0-beta.5
|
||||
## 09/22/2025
|
||||
|
||||
1. [](#new)
|
||||
* Deferred Extension support in Forked version of Twig 3
|
||||
* Added separate `strict_mode.twig2_compat` and `strict_mode.twig3_compat` toggles to manage auto-escape behaviour and automatic Twig 3 compatible template rewrites
|
||||
1. [](#bugfix)
|
||||
* Fix for cache blowing up when upgrading from 1.7 to 1.8 via CLI
|
||||
|
||||
# v1.8.0-beta.4
|
||||
## 01/27/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed a PHP compatibility issue with `AbstractLazyCollection`
|
||||
1. [](#improved)
|
||||
* Global PHP 8.2 code optimizations
|
||||
* More PHP 8.4 compatibility fixes
|
||||
* Twig 2.x forked to getgrav/twig 2.x for PHP 8.4 compatibility
|
||||
* Switch to cache@v4 + limit PHP version for Github actions
|
||||
* Trigger testing Github action for Grav 1.8
|
||||
* Merge latest Grav 1.7 fixes into Grav 1.8
|
||||
|
||||
# v1.8.0-beta.3
|
||||
## 11/21/2024
|
||||
|
||||
1. [](#improved)
|
||||
* Updated composer libraries to latest versions for compatibility fixes
|
||||
|
||||
# v1.8.0-beta.2
|
||||
## 10/28/2024
|
||||
|
||||
@@ -17,7 +244,7 @@
|
||||
## 10/23/2024
|
||||
|
||||
1. [](#new)
|
||||
* Set minimum requirements to **PHP 8.2**
|
||||
* Set minimum requirements to **PHP 8.3**
|
||||
* Updated to **Twig 2.14**
|
||||
* Updated to **Symfony 6.4**
|
||||
* Updated to **Monolog 2.3**
|
||||
@@ -28,6 +255,67 @@
|
||||
* Removed `system.umask_fix` setting for security reasons
|
||||
* Support phpstan level 6 in Framework classes
|
||||
|
||||
|
||||
# v1.7.50
|
||||
## UNRELEASED
|
||||
|
||||
1. [](#new)
|
||||
* Added staged self-upgrade pipeline with manifest snapshots and atomic swaps for Grav core updates.
|
||||
* Introduced recovery mode with token-gated UI, plugin quarantine, and CLI rollback support.
|
||||
* Added `bin/gpm preflight` compatibility scanner and `bin/gpm rollback` utility.
|
||||
|
||||
# v1.7.49.5
|
||||
## 09/10/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Backup not honoring ignored paths [#3952](https://github.com/getgrav/grav/issues/3952)
|
||||
|
||||
# v1.7.49.4
|
||||
## 09/03/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed cron force running jobs severy minute! [#3951](https://github.com/getgrav/grav/issues/3951)
|
||||
|
||||
# v1.7.49.3
|
||||
## 09/02/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed an error in ZipArchive that was causing issues on some systems
|
||||
* Fixed namespace change for `Cron\Expression`
|
||||
* Removed broken cron install field... use 'instructions' instead
|
||||
* Fixed duplicate jobs listing in some CLI commands
|
||||
|
||||
# v1.7.49.2
|
||||
## 08/28/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fix translation of key for image adapter [#3944](https://github.com/getgrav/grav/pull/3944)
|
||||
|
||||
# v1.7.49.1
|
||||
## 08/25/2025
|
||||
|
||||
1. [](#new)
|
||||
* Rerelease to include all updated plugins/theme etc.
|
||||
|
||||
# v1.7.49
|
||||
## 08/25/2025
|
||||
|
||||
1. [](#new)
|
||||
* Revamped Grav Scheduler to support webhook to call call scheduler + concurrent jobs + jobs queue + logging, and other improvements
|
||||
* Revamped Grav Cache purge capabilities to only clear obsolete old cache items
|
||||
* Added full imagick support in Grav Image library
|
||||
* Added support for Validate `match` and `match_any` in forms
|
||||
1. [](#improved)
|
||||
* Handle empty values on require with ignore fields in Forms
|
||||
* Use `actions/cache@v4` in github workflows
|
||||
* Use `actions/checkout@v4`in github workflows [#3867](https://github.com/getgrav/grav/pull/3867)
|
||||
* Update code block in README.md [#3886](https://github.com/getgrav/grav/pull/3886)
|
||||
* Updated vendor libs to latest
|
||||
1. [](#bugfix)
|
||||
* Bug in `exif_read_data` [#3878](https://github.com/getgrav/grav/pull/3878)
|
||||
* Fix parser error in URI: [#3894](https://github.com/getgrav/grav/issues/3894)
|
||||
|
||||
|
||||
# v1.7.48
|
||||
## 10/28/2024
|
||||
|
||||
|
||||
36
README.md
36
README.md
@@ -12,7 +12,7 @@ The underlying architecture of Grav is designed to use well-established and _bes
|
||||
* [Markdown](https://en.wikipedia.org/wiki/Markdown): for easy content creation
|
||||
* [YAML](https://yaml.org): for simple configuration
|
||||
* [Parsedown](https://parsedown.org/): for fast Markdown and Markdown Extra support
|
||||
* [Doctrine Cache](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html): layer for performance
|
||||
* [Symfony Cache](https://symfony.com/doc/current/components/cache.html): backend layer for performance
|
||||
* [Pimple Dependency Injection Container](https://github.com/silexphp/Pimple): for extensibility and maintainability
|
||||
* [Symfony Event Dispatcher](https://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling
|
||||
* [Symfony Console](https://symfony.com/doc/current/components/console/introduction.html): for CLI interface
|
||||
@@ -20,7 +20,7 @@ The underlying architecture of Grav is designed to use well-established and _bes
|
||||
|
||||
# Requirements
|
||||
|
||||
- PHP 7.3.6 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
|
||||
- PHP 8.3 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
|
||||
- Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
|
||||
|
||||
# Documentation
|
||||
@@ -39,22 +39,22 @@ You can download a **ready-built** package from the [Downloads page on https://g
|
||||
|
||||
You can create a new project with the latest **stable** Grav release with the following command:
|
||||
|
||||
```
|
||||
$ composer create-project getgrav/grav ~/webroot/grav
|
||||
```bash
|
||||
composer create-project getgrav/grav ~/webroot/grav
|
||||
```
|
||||
|
||||
### From GitHub
|
||||
|
||||
1. Clone the Grav repository from [https://github.com/getgrav/grav]() to a folder in the webroot of your server, e.g. `~/webroot/grav`. Launch a **terminal** or **console** and navigate to the webroot folder:
|
||||
```
|
||||
$ cd ~/webroot
|
||||
$ git clone https://github.com/getgrav/grav.git
|
||||
```bash
|
||||
cd ~/webroot
|
||||
git clone https://github.com/getgrav/grav.git
|
||||
```
|
||||
|
||||
2. Install the **plugin** and **theme dependencies** by using the [Grav CLI application](https://learn.getgrav.org/advanced/grav-cli) `bin/grav`:
|
||||
```
|
||||
$ cd ~/webroot/grav
|
||||
$ bin/grav install
|
||||
```bash
|
||||
cd ~/webroot/grav
|
||||
bin/grav install
|
||||
```
|
||||
|
||||
Check out the [install procedures](https://learn.getgrav.org/basics/installation) for more information.
|
||||
@@ -63,28 +63,28 @@ Check out the [install procedures](https://learn.getgrav.org/basics/installation
|
||||
|
||||
You can download [plugins](https://getgrav.org/downloads/plugins) or [themes](https://getgrav.org/downloads/themes) manually from the appropriate tab on the [Downloads page on https://getgrav.org](https://getgrav.org/downloads), but the preferred solution is to use the [Grav Package Manager](https://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
|
||||
|
||||
```
|
||||
$ bin/gpm index
|
||||
```bash
|
||||
bin/gpm index
|
||||
```
|
||||
|
||||
This will display all the available plugins and then you can install one or more with:
|
||||
|
||||
```
|
||||
$ bin/gpm install <plugin/theme>
|
||||
```bash
|
||||
bin/gpm install <plugin/theme>
|
||||
```
|
||||
|
||||
# Updating
|
||||
|
||||
To update Grav you should use the [Grav Package Manager](https://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
|
||||
|
||||
```
|
||||
$ bin/gpm selfupgrade
|
||||
```bash
|
||||
bin/gpm selfupgrade
|
||||
```
|
||||
|
||||
To update plugins and themes:
|
||||
|
||||
```
|
||||
$ bin/gpm update
|
||||
```bash
|
||||
bin/gpm update
|
||||
```
|
||||
|
||||
## Upgrading from older version
|
||||
|
||||
@@ -12,7 +12,7 @@ We are focusing our security updates on the following versions
|
||||
|
||||
## :pushpin: Note on Security Severity
|
||||
|
||||
> NOTE: Please use the following guidlines when selecting a **Severity**. Submitted advisories that are marked **High** or **Critical** that don't meet the guidelines below will be cliosed.
|
||||
> NOTE: Please use the following guidelines when selecting a **Severity**. Submitted advisories that are marked **High** or **Critical** that don't meet the guidelines below will be closed.
|
||||
|
||||
* **CRITICAL** - no account required, can modify content, or run malicious code or nefarious activity without any access.
|
||||
* **HIGH** - publisher level account able to run malicious code or nefarious activity, or other high level security things.
|
||||
|
||||
209
bin/build-test-update.php
Executable file
209
bin/build-test-update.php
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
if (!\defined('GRAV_ROOT')) {
|
||||
\define('GRAV_ROOT', realpath(__DIR__ . '/..') ?: getcwd());
|
||||
}
|
||||
|
||||
if (!\extension_loaded('zip')) {
|
||||
fwrite(STDERR, "The PHP zip extension is required.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$options = getopt('', [
|
||||
'version:',
|
||||
'output::',
|
||||
'port::',
|
||||
'base-url::',
|
||||
'serve',
|
||||
]);
|
||||
|
||||
if (!isset($options['version'])) {
|
||||
fwrite(
|
||||
STDERR,
|
||||
"Usage: php bin/build-test-update.php --version=1.7.999 [--output=tmp/test-gpm] [--port=8043] [--base-url=http://127.0.0.1:8043] [--serve]\n"
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$version = trim((string) $options['version']);
|
||||
if ($version === '') {
|
||||
fwrite(STDERR, "A non-empty --version value is required.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = GRAV_ROOT;
|
||||
|
||||
$output = $options['output'] ?? $root . '/tmp/test-gpm';
|
||||
if (!str_starts_with($output, DIRECTORY_SEPARATOR)) {
|
||||
$output = $root . '/' . ltrim($output, '/');
|
||||
}
|
||||
$output = rtrim($output, DIRECTORY_SEPARATOR);
|
||||
|
||||
$defaultPort = isset($options['port']) ? (int) $options['port'] : 8043;
|
||||
$baseUrl = $options['base-url'] ?? sprintf('http://127.0.0.1:%d', $defaultPort);
|
||||
$serve = array_key_exists('serve', $options);
|
||||
|
||||
Folder::create($output);
|
||||
|
||||
$downloadName = sprintf('grav-update-%s.zip', $version);
|
||||
$zipPath = $output . '/' . $downloadName;
|
||||
$jsonPath = $output . '/grav.json';
|
||||
$zipPrefix = 'grav-update/';
|
||||
|
||||
$excludeDirs = [
|
||||
'.build',
|
||||
'.crush',
|
||||
'.ddev',
|
||||
'.git',
|
||||
'.github',
|
||||
'.gitlab',
|
||||
'.circleci',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'.pytest_cache',
|
||||
'backup',
|
||||
'cache',
|
||||
'images',
|
||||
'logs',
|
||||
'node_modules',
|
||||
'tests',
|
||||
'tmp',
|
||||
'user',
|
||||
];
|
||||
|
||||
$excludeFiles = [
|
||||
'.htaccess',
|
||||
'.DS_Store',
|
||||
'robots.txt',
|
||||
];
|
||||
|
||||
$directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$filtered = new RecursiveCallbackFilterIterator(
|
||||
$directory,
|
||||
function (SplFileInfo $current) use ($root, $excludeDirs, $excludeFiles): bool {
|
||||
$relative = ltrim(str_replace($root, '', $current->getPathname()), DIRECTORY_SEPARATOR);
|
||||
$relative = str_replace('\\', '/', $relative);
|
||||
|
||||
if ($relative === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains($relative, '..')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($excludeDirs as $prefix) {
|
||||
$prefix = trim($prefix, '/');
|
||||
if ($prefix === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($relative === $prefix || str_starts_with($relative, $prefix . '/')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($current->getFilename(), $excludeFiles, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new RuntimeException(sprintf('Unable to open archive at %s', $zipPath));
|
||||
}
|
||||
|
||||
$zip->addEmptyDir($zipPrefix);
|
||||
|
||||
$iterator = new RecursiveIteratorIterator($filtered, RecursiveIteratorIterator::SELF_FIRST);
|
||||
/** @var SplFileInfo $fileInfo */
|
||||
foreach ($iterator as $fileInfo) {
|
||||
$fullPath = $fileInfo->getPathname();
|
||||
$relative = ltrim(str_replace($root, '', $fullPath), DIRECTORY_SEPARATOR);
|
||||
$relative = str_replace('\\', '/', $relative);
|
||||
$targetPath = $zipPrefix . $relative;
|
||||
|
||||
if ($fileInfo->isDir()) {
|
||||
$zip->addEmptyDir(rtrim($targetPath, '/') . '/');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fileInfo->isLink()) {
|
||||
$target = readlink($fullPath);
|
||||
$zip->addFromString($targetPath, $target === false ? '' : $target);
|
||||
$zip->setExternalAttributesName($targetPath, ZipArchive::OPSYS_UNIX, 0120000 << 16);
|
||||
continue;
|
||||
}
|
||||
|
||||
$zip->addFile($fullPath, $targetPath);
|
||||
|
||||
$perms = @fileperms($fullPath);
|
||||
if ($perms !== false) {
|
||||
$zip->setExternalAttributesName($targetPath, ZipArchive::OPSYS_UNIX, ($perms & 0xFFFF) << 16);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
$size = filesize($zipPath);
|
||||
$sha256 = hash_file('sha256', $zipPath);
|
||||
$timestamp = date('c');
|
||||
$downloadUrl = rtrim($baseUrl, '/') . '/' . rawurlencode($downloadName);
|
||||
|
||||
$manifest = [
|
||||
'version' => $version,
|
||||
'date' => $timestamp,
|
||||
'min_php' => '8.3.0',
|
||||
'assets' => [
|
||||
'grav-update' => [
|
||||
'name' => $downloadName,
|
||||
'slug' => 'grav-update',
|
||||
'version' => $version,
|
||||
'date' => $timestamp,
|
||||
'testing' => false,
|
||||
'description' => 'Local test update package generated for safe-upgrade validation.',
|
||||
'download' => $downloadUrl,
|
||||
'size' => $size,
|
||||
'checksum' => 'sha256:' . $sha256,
|
||||
'sha256' => $sha256,
|
||||
'host' => parse_url($downloadUrl, PHP_URL_HOST),
|
||||
],
|
||||
],
|
||||
'changelog' => [
|
||||
$version => [
|
||||
'date' => $timestamp,
|
||||
'content' => "- Local test update package generated by build-test-update.\n",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
file_put_contents($jsonPath, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
|
||||
$manifestUrl = rtrim($baseUrl, '/') . '/grav.json';
|
||||
|
||||
echo "Update package created at: {$zipPath}\n";
|
||||
echo "Manifest written to: {$jsonPath}\n";
|
||||
echo "Manifest URL: {$manifestUrl}\n";
|
||||
echo "Download URL: {$downloadUrl}\n";
|
||||
echo "Archive size: {$size} bytes\n";
|
||||
echo "SHA256: {$sha256}\n";
|
||||
|
||||
if ($serve) {
|
||||
$host = parse_url($baseUrl, PHP_URL_HOST) ?: '127.0.0.1';
|
||||
$port = parse_url($baseUrl, PHP_URL_PORT) ?: $defaultPort;
|
||||
$command = sprintf('php -S %s:%d -t %s', $host, $port, escapeshellarg($output));
|
||||
echo "\nServing files using PHP built-in server. Press Ctrl+C to stop.\n";
|
||||
echo $command . "\n\n";
|
||||
passthru($command);
|
||||
}
|
||||
Binary file not shown.
2
bin/gpm
2
bin/gpm
@@ -2,7 +2,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
2
bin/grav
2
bin/grav
@@ -2,7 +2,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
634
bin/restore
Executable file
634
bin/restore
Executable file
@@ -0,0 +1,634 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Grav Snapshot Restore Utility
|
||||
*
|
||||
* Lightweight CLI that can list and apply safe-upgrade snapshots without
|
||||
* bootstrapping the full Grav application (or any plugins).
|
||||
*/
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
|
||||
define('GRAV_CLI', true);
|
||||
define('GRAV_REQUEST_TIME', microtime(true));
|
||||
|
||||
if (!file_exists($root . '/vendor/autoload.php')) {
|
||||
fwrite(STDERR, "Unable to locate vendor/autoload.php. Run composer install first.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$autoload = require $root . '/vendor/autoload.php';
|
||||
|
||||
if (!file_exists($root . '/index.php')) {
|
||||
fwrite(STDERR, "FATAL: Must be run from Grav root directory.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Recovery\RecoveryManager;
|
||||
use Grav\Common\Upgrade\SafeUpgradeService;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
const RESTORE_USAGE = <<<USAGE
|
||||
Grav Restore Utility
|
||||
|
||||
Usage:
|
||||
bin/restore list [--staging-root=/absolute/path]
|
||||
Lists all available snapshots (most recent first).
|
||||
|
||||
bin/restore apply <snapshot-id> [--staging-root=/absolute/path]
|
||||
Restores the specified snapshot created by safe-upgrade.
|
||||
|
||||
bin/restore remove [<snapshot-id> ...] [--staging-root=/absolute/path]
|
||||
Deletes one or more snapshots (interactive selection when no id provided).
|
||||
|
||||
bin/restore snapshot [--label=\"optional description\"] [--staging-root=/absolute/path]
|
||||
Creates a manual snapshot of the current Grav core files.
|
||||
|
||||
bin/restore recovery [status|clear]
|
||||
Shows the recovery flag context or clears it.
|
||||
|
||||
Options:
|
||||
--staging-root Overrides the staging directory (defaults to configured value).
|
||||
--label Optional label to store with the manual snapshot.
|
||||
|
||||
Examples:
|
||||
bin/restore list
|
||||
bin/restore apply stage-68eff31cc4104
|
||||
bin/restore apply stage-68eff31cc4104 --staging-root=/var/grav-backups
|
||||
bin/restore snapshot --label=\"Before plugin install\"
|
||||
bin/restore recovery status
|
||||
bin/restore recovery clear
|
||||
USAGE;
|
||||
|
||||
/**
|
||||
* @param array $args
|
||||
* @return array{command:string,arguments:array,options:array}
|
||||
*/
|
||||
function parseArguments(array $args): array
|
||||
{
|
||||
array_shift($args); // remove script name
|
||||
|
||||
$command = null;
|
||||
$arguments = [];
|
||||
$options = [];
|
||||
|
||||
while ($args) {
|
||||
$arg = array_shift($args);
|
||||
if (strncmp($arg, '--', 2) === 0) {
|
||||
$parts = explode('=', substr($arg, 2), 2);
|
||||
$name = $parts[0] ?? '';
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$value = $parts[1] ?? null;
|
||||
if ($value === null && $args && substr($args[0], 0, 2) !== '--') {
|
||||
$value = array_shift($args);
|
||||
}
|
||||
$options[$name] = $value ?? true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $command) {
|
||||
$command = $arg;
|
||||
} else {
|
||||
$arguments[] = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $command) {
|
||||
$command = 'interactive';
|
||||
}
|
||||
|
||||
return [
|
||||
'command' => $command,
|
||||
'arguments' => $arguments,
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return SafeUpgradeService
|
||||
*/
|
||||
function createUpgradeService(array $options): SafeUpgradeService
|
||||
{
|
||||
$serviceOptions = ['root' => GRAV_ROOT];
|
||||
|
||||
if (isset($options['staging-root']) && is_string($options['staging-root']) && $options['staging-root'] !== '') {
|
||||
$serviceOptions['staging_root'] = $options['staging-root'];
|
||||
}
|
||||
|
||||
return new SafeUpgradeService($serviceOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id:string,label:?string,source_version:?string,target_version:?string,created_at:int}>
|
||||
*/
|
||||
function loadSnapshots(): array
|
||||
{
|
||||
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
||||
if (!is_dir($manifestDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob($manifestDir . '/*.json') ?: [];
|
||||
rsort($files);
|
||||
|
||||
$snapshots = [];
|
||||
foreach ($files as $file) {
|
||||
$decoded = json_decode(file_get_contents($file) ?: '', true);
|
||||
if (!is_array($decoded) || empty($decoded['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshots[] = [
|
||||
'id' => $decoded['id'],
|
||||
'label' => $decoded['label'] ?? null,
|
||||
'source_version' => $decoded['source_version'] ?? null,
|
||||
'target_version' => $decoded['target_version'] ?? null,
|
||||
'created_at' => (int)($decoded['created_at'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{id:string,label:?string,source_version:?string,target_version:?string,created_at:int}> $snapshots
|
||||
* @return string
|
||||
*/
|
||||
function formatSnapshotListLine(array $snapshot): string
|
||||
{
|
||||
$restoreVersion = $snapshot['source_version'] ?? $snapshot['target_version'] ?? 'unknown';
|
||||
$timeLabel = formatSnapshotTimestamp($snapshot['created_at']);
|
||||
$label = $snapshot['label'] ?? null;
|
||||
$display = $label ? sprintf('%s [%s]', $label, $snapshot['id']) : $snapshot['id'];
|
||||
|
||||
return sprintf('%s (restore to Grav %s, %s)', $display, $restoreVersion, $timeLabel);
|
||||
}
|
||||
|
||||
function formatSnapshotTimestamp(int $timestamp): string
|
||||
{
|
||||
if ($timestamp <= 0) {
|
||||
return 'time unknown';
|
||||
}
|
||||
|
||||
try {
|
||||
$timezone = resolveTimezone();
|
||||
$dt = new DateTime('@' . $timestamp);
|
||||
$dt->setTimezone($timezone);
|
||||
$formatted = $dt->format('Y-m-d H:i:s T');
|
||||
} catch (\Throwable $e) {
|
||||
$formatted = date('Y-m-d H:i:s T', $timestamp);
|
||||
}
|
||||
|
||||
return $formatted . ' (' . formatRelative(time() - $timestamp) . ')';
|
||||
}
|
||||
|
||||
function resolveTimezone(): DateTimeZone
|
||||
{
|
||||
static $resolved = null;
|
||||
if ($resolved instanceof DateTimeZone) {
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
$timezone = null;
|
||||
$configFile = GRAV_ROOT . '/user/config/system.yaml';
|
||||
if (is_file($configFile)) {
|
||||
try {
|
||||
$data = Yaml::parse(file_get_contents($configFile) ?: '') ?: [];
|
||||
if (!empty($data['system']['timezone']) && is_string($data['system']['timezone'])) {
|
||||
$timezone = $data['system']['timezone'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore parse errors, fallback below
|
||||
}
|
||||
}
|
||||
|
||||
if (!$timezone) {
|
||||
$timezone = ini_get('date.timezone') ?: 'UTC';
|
||||
}
|
||||
|
||||
try {
|
||||
$resolved = new DateTimeZone($timezone);
|
||||
} catch (\Throwable $e) {
|
||||
$resolved = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
function formatRelative(int $seconds): string
|
||||
{
|
||||
if ($seconds < 5) {
|
||||
return 'just now';
|
||||
}
|
||||
$negative = $seconds < 0;
|
||||
$seconds = abs($seconds);
|
||||
$units = [
|
||||
31536000 => 'y',
|
||||
2592000 => 'mo',
|
||||
604800 => 'w',
|
||||
86400 => 'd',
|
||||
3600 => 'h',
|
||||
60 => 'm',
|
||||
1 => 's',
|
||||
];
|
||||
foreach ($units as $size => $label) {
|
||||
if ($seconds >= $size) {
|
||||
$value = (int)floor($seconds / $size);
|
||||
$suffix = $label === 'mo' ? 'month' : ($label === 'y' ? 'year' : ($label === 'w' ? 'week' : ($label === 'd' ? 'day' : ($label === 'h' ? 'hour' : ($label === 'm' ? 'minute' : 'second')))));
|
||||
if ($value !== 1) {
|
||||
$suffix .= 's';
|
||||
}
|
||||
$phrase = $value . ' ' . $suffix;
|
||||
return $negative ? 'in ' . $phrase : $phrase . ' ago';
|
||||
}
|
||||
}
|
||||
|
||||
return $negative ? 'in 0 seconds' : '0 seconds ago';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $snapshotId
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
function applySnapshot(string $snapshotId, array $options): void
|
||||
{
|
||||
try {
|
||||
$service = createUpgradeService($options);
|
||||
$manifest = $service->rollback($snapshotId);
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "Restore failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!$manifest) {
|
||||
fwrite(STDERR, "Snapshot {$snapshotId} not found.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
|
||||
echo "Restored snapshot {$snapshotId} (Grav {$version}).\n";
|
||||
if (!empty($manifest['id'])) {
|
||||
echo "Snapshot manifest: {$manifest['id']}\n";
|
||||
}
|
||||
if (!empty($manifest['backup_path'])) {
|
||||
echo "Snapshot path: {$manifest['backup_path']}\n";
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
function createManualSnapshot(array $options): void
|
||||
{
|
||||
$label = null;
|
||||
if (isset($options['label']) && is_string($options['label'])) {
|
||||
$label = trim($options['label']);
|
||||
if ($label === '') {
|
||||
$label = null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$service = createUpgradeService($options);
|
||||
$manifest = $service->createSnapshot($label);
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "Snapshot creation failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$snapshotId = $manifest['id'] ?? null;
|
||||
if (!$snapshotId) {
|
||||
$snapshotId = 'unknown';
|
||||
}
|
||||
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
|
||||
|
||||
echo "Created snapshot {$snapshotId} (Grav {$version}).\n";
|
||||
if ($label) {
|
||||
echo "Label: {$label}\n";
|
||||
}
|
||||
if (!empty($manifest['backup_path'])) {
|
||||
echo "Snapshot path: {$manifest['backup_path']}\n";
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{id:string,source_version:?string,target_version:?string,created_at:int}> $snapshots
|
||||
* @return string|null
|
||||
*/
|
||||
function promptSnapshotSelection(array $snapshots): ?string
|
||||
{
|
||||
echo "Available snapshots:\n";
|
||||
foreach ($snapshots as $index => $snapshot) {
|
||||
$line = formatSnapshotListLine($snapshot);
|
||||
$number = $index + 1;
|
||||
echo sprintf(" [%d] %s\n", $number, $line);
|
||||
}
|
||||
|
||||
$default = $snapshots[0]['id'];
|
||||
echo "\nSelect a snapshot to restore [1]: ";
|
||||
$input = trim((string)fgets(STDIN));
|
||||
|
||||
if ($input === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (ctype_digit($input)) {
|
||||
$idx = (int)$input - 1;
|
||||
if (isset($snapshots[$idx])) {
|
||||
return $snapshots[$idx]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($snapshots as $snapshot) {
|
||||
if (strcasecmp($snapshot['id'], $input) === 0) {
|
||||
return $snapshot['id'];
|
||||
}
|
||||
}
|
||||
|
||||
echo "Invalid selection. Aborting.\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{id:string,source_version:?string,target_version:?string,created_at:int}> $snapshots
|
||||
* @return array<string>
|
||||
*/
|
||||
function promptSnapshotsRemoval(array $snapshots): array
|
||||
{
|
||||
echo "Available snapshots:\n";
|
||||
foreach ($snapshots as $index => $snapshot) {
|
||||
$line = formatSnapshotListLine($snapshot);
|
||||
$number = $index + 1;
|
||||
echo sprintf(" [%d] %s\n", $number, $line);
|
||||
}
|
||||
|
||||
echo "\nSelect snapshots to remove (comma or space separated numbers / ids, 'all' for everything, empty to cancel): ";
|
||||
$input = trim((string)fgets(STDIN));
|
||||
|
||||
if ($input === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$inputLower = strtolower($input);
|
||||
if ($inputLower === 'all' || $inputLower === '*') {
|
||||
return array_values(array_unique(array_column($snapshots, 'id')));
|
||||
}
|
||||
|
||||
$tokens = preg_split('/[\\s,]+/', $input, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$selected = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (ctype_digit($token)) {
|
||||
$idx = (int)$token - 1;
|
||||
if (isset($snapshots[$idx])) {
|
||||
$selected[] = $snapshots[$idx]['id'];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($snapshots as $snapshot) {
|
||||
if (strcasecmp($snapshot['id'], $token) === 0) {
|
||||
$selected[] = $snapshot['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($selected)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $snapshotId
|
||||
* @return array{success:bool,message:string}
|
||||
*/
|
||||
function removeSnapshot(string $snapshotId): array
|
||||
{
|
||||
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
||||
$manifestPath = $manifestDir . '/' . $snapshotId . '.json';
|
||||
if (!is_file($manifestPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Snapshot {$snapshotId} not found."
|
||||
];
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($manifestPath) ?: '', true);
|
||||
if (!is_array($manifest)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Snapshot {$snapshotId} manifest is invalid."
|
||||
];
|
||||
}
|
||||
|
||||
$pathsToDelete = [];
|
||||
foreach (['package_path', 'backup_path'] as $key) {
|
||||
if (!empty($manifest[$key]) && is_string($manifest[$key])) {
|
||||
$pathsToDelete[] = $manifest[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($pathsToDelete as $path) {
|
||||
if (!$path) {
|
||||
continue;
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (is_dir($path)) {
|
||||
Folder::delete($path);
|
||||
} else {
|
||||
@unlink($path);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errors[] = "Unable to remove {$path}: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!@unlink($manifestPath)) {
|
||||
$errors[] = "Unable to delete manifest file {$manifestPath}.";
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => implode(' ', $errors)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Removed snapshot {$snapshotId}."
|
||||
];
|
||||
}
|
||||
|
||||
$cli = parseArguments($argv);
|
||||
$command = $cli['command'];
|
||||
$arguments = $cli['arguments'];
|
||||
$options = $cli['options'];
|
||||
|
||||
switch ($command) {
|
||||
case 'interactive':
|
||||
$snapshots = loadSnapshots();
|
||||
if (!$snapshots) {
|
||||
echo "No snapshots found. Run bin/gpm self-upgrade (with safe upgrade enabled) to create one.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$selection = promptSnapshotSelection($snapshots);
|
||||
if (!$selection) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
applySnapshot($selection, $options);
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
$snapshots = loadSnapshots();
|
||||
if (!$snapshots) {
|
||||
echo "No snapshots found. Run bin/gpm self-upgrade (with safe upgrade enabled) to create one.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "Available snapshots:\n";
|
||||
foreach ($snapshots as $snapshot) {
|
||||
echo ' - ' . formatSnapshotListLine($snapshot) . "\n";
|
||||
}
|
||||
exit(0);
|
||||
|
||||
case 'remove':
|
||||
$snapshots = loadSnapshots();
|
||||
if (!$snapshots) {
|
||||
echo "No snapshots found. Nothing to remove.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$selectedIds = [];
|
||||
if ($arguments) {
|
||||
foreach ($arguments as $arg) {
|
||||
if (!$arg) {
|
||||
continue;
|
||||
}
|
||||
$selectedIds[] = $arg;
|
||||
}
|
||||
} else {
|
||||
$selectedIds = promptSnapshotsRemoval($snapshots);
|
||||
if (!$selectedIds) {
|
||||
echo "No snapshots selected. Aborting.\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$selectedIds = array_values(array_unique($selectedIds));
|
||||
echo "Snapshots selected for removal:\n";
|
||||
foreach ($selectedIds as $id) {
|
||||
echo " - {$id}\n";
|
||||
}
|
||||
|
||||
$autoConfirm = isset($options['yes']) || isset($options['y']);
|
||||
if (!$autoConfirm) {
|
||||
echo "\nThis action cannot be undone. Proceed? [y/N] ";
|
||||
$confirmation = strtolower(trim((string)fgets(STDIN)));
|
||||
if (!in_array($confirmation, ['y', 'yes'], true)) {
|
||||
echo "Aborted.\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
foreach ($selectedIds as $id) {
|
||||
$result = removeSnapshot($id);
|
||||
echo $result['message'] . "\n";
|
||||
if ($result['success']) {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
|
||||
exit($success > 0 ? 0 : 1);
|
||||
|
||||
case 'apply':
|
||||
$snapshotId = $arguments[0] ?? null;
|
||||
if (!$snapshotId) {
|
||||
echo "Missing snapshot id.\n\n" . RESTORE_USAGE . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
applySnapshot($snapshotId, $options);
|
||||
break;
|
||||
|
||||
case 'snapshot':
|
||||
createManualSnapshot($options);
|
||||
break;
|
||||
|
||||
case 'recovery':
|
||||
$action = strtolower($arguments[0] ?? 'status');
|
||||
$manager = new RecoveryManager(GRAV_ROOT);
|
||||
|
||||
switch ($action) {
|
||||
case 'clear':
|
||||
if ($manager->isActive()) {
|
||||
$manager->clear();
|
||||
echo "Recovery flag cleared.\n";
|
||||
} else {
|
||||
echo "Recovery mode is not active.\n";
|
||||
}
|
||||
exit(0);
|
||||
|
||||
case 'status':
|
||||
if (!$manager->isActive()) {
|
||||
echo "Recovery mode is not active.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$context = $manager->getContext();
|
||||
if (!$context) {
|
||||
echo "Recovery flag present but context could not be parsed.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$created = isset($context['created_at']) ? date('c', (int)$context['created_at']) : 'unknown';
|
||||
$token = $context['token'] ?? '(missing)';
|
||||
$message = $context['message'] ?? '(no message)';
|
||||
$plugin = $context['plugin'] ?? '(none detected)';
|
||||
$file = $context['file'] ?? '(unknown file)';
|
||||
$line = $context['line'] ?? '(unknown line)';
|
||||
|
||||
echo "Recovery flag context:\n";
|
||||
echo " Token: {$token}\n";
|
||||
echo " Message: {$message}\n";
|
||||
echo " Plugin: {$plugin}\n";
|
||||
echo " File: {$file}\n";
|
||||
echo " Line: {$line}\n";
|
||||
echo " Created: {$created}\n";
|
||||
|
||||
$window = $manager->getUpgradeWindow();
|
||||
if ($window) {
|
||||
$expires = isset($window['expires_at']) ? date('c', (int)$window['expires_at']) : 'unknown';
|
||||
$reason = $window['reason'] ?? '(unknown)';
|
||||
echo " Window: active ({$reason}, expires {$expires})\n";
|
||||
} else {
|
||||
echo " Window: inactive\n";
|
||||
}
|
||||
exit(0);
|
||||
|
||||
default:
|
||||
echo "Unknown recovery action: {$action}\n\n" . RESTORE_USAGE . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
echo RESTORE_USAGE . "\n";
|
||||
exit($command === 'help' ? 0 : 1);
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"homepage": "https://getgrav.org",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php": "^8.3",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-curl": "*",
|
||||
@@ -24,24 +24,22 @@
|
||||
"symfony/polyfill-iconv": "^1.24",
|
||||
"symfony/polyfill-php80": "^1.24",
|
||||
"symfony/polyfill-php81": "^1.24",
|
||||
"psr/simple-cache": "^1.0",
|
||||
"psr/http-message": "^1.1",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"psr/container": "^1.1",
|
||||
"psr/log": "^1.1",
|
||||
"symfony/cache": "^6.4",
|
||||
"symfony/yaml": "^6.4",
|
||||
"symfony/console": "^6.4",
|
||||
"symfony/event-dispatcher": "^6.4",
|
||||
"symfony/var-exporter": "^6.4",
|
||||
"symfony/var-dumper": "^6.4",
|
||||
"symfony/process": "^6.4",
|
||||
"symfony/http-client": "^6.4",
|
||||
"twig/twig": "^2.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"doctrine/cache": "^2.2",
|
||||
"psr/container": "^1.1 || ^2.0",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0",
|
||||
"symfony/cache": "^6.4 || ^7.0",
|
||||
"symfony/yaml": "^6.4 || ^7.0",
|
||||
"symfony/console": "^6.4 || ^7.0",
|
||||
"symfony/event-dispatcher": "^6.4 || ^7.0",
|
||||
"symfony/var-exporter": "^6.4 || ^7.0",
|
||||
"symfony/var-dumper": "^6.4 || ^7.0",
|
||||
"symfony/process": "^6.4 || ^7.0",
|
||||
"symfony/http-client": "^6.4 || ^7.0",
|
||||
"twig/twig": "3.x-dev",
|
||||
"monolog/monolog": "^3.0",
|
||||
"doctrine/collections": "^2.2",
|
||||
"pimple/pimple": "~3.5.0",
|
||||
"nyholm/psr7-server": "^1.1",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"erusev/parsedown": "^1.7",
|
||||
@@ -49,36 +47,45 @@
|
||||
"rockettheme/toolbox": "v2.x-dev",
|
||||
"composer/ca-bundle": "^1.5",
|
||||
"composer/semver": "^3.4",
|
||||
"dragonmantank/cron-expression": "^3.0",
|
||||
"dragonmantank/cron-expression": "^3.3",
|
||||
"willdurand/negotiation": "^3.1",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"tubalmartin/cssmin": "^4.1",
|
||||
"tedivm/jshrink": "^1.7",
|
||||
"donatj/phpuseragentparser": "~1.9",
|
||||
"guzzlehttp/psr7": "^2.7",
|
||||
"filp/whoops": "~2.16",
|
||||
"itsgoingd/clockwork": "dev-master",
|
||||
"maximebf/debugbar": "~1.23",
|
||||
"itsgoingd/clockwork": "^5.3",
|
||||
"php-debugbar/php-debugbar": "~2.1",
|
||||
"getgrav/image": "^4.0",
|
||||
"getgrav/cache": "^2.0",
|
||||
"antoligy/dom-string-iterators": "^1.0",
|
||||
"miljar/php-exif": "^0.6",
|
||||
"league/climate": "^3.8",
|
||||
"league/climate": "^3.10",
|
||||
"multiavatar/multiavatar-php": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/codeception": "^5.1",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.2",
|
||||
"phpunit/php-code-coverage": "~9.2",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"getgrav/markdowndocs": "^2.0",
|
||||
"codeception/module-asserts": "*",
|
||||
"codeception/module-phpbrowser": "*",
|
||||
"rector/rector": "^1.2"
|
||||
"rector/rector": "^2.1"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/rockettheme/toolbox"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/getgrav/twig"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/getgrav/parsedown"
|
||||
}
|
||||
],
|
||||
"replace": {
|
||||
@@ -99,7 +106,7 @@
|
||||
"config": {
|
||||
"apcu-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.2"
|
||||
"php": "8.3"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -107,7 +114,8 @@
|
||||
"Grav\\": "system/src/Grav",
|
||||
"Doctrine\\": "system/src/Doctrine",
|
||||
"RocketTheme\\": "system/src/RocketTheme",
|
||||
"Twig\\": "system/src/Twig"
|
||||
"Twig\\": "system/src/Twig",
|
||||
"Pimple\\": "system/src/Pimple"
|
||||
},
|
||||
"files": [
|
||||
"system/defines.php",
|
||||
@@ -126,13 +134,15 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
|
||||
"api-18": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.18.md",
|
||||
"post-create-project-cmd": "bin/grav install",
|
||||
"rector": "vendor/bin/rector",
|
||||
"rector:php-compat": "@php vendor/bin/rector process --config=system/rector.php --ansi --no-progress-bar",
|
||||
"phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon --memory-limit=720M system/src",
|
||||
"phpstan-framework": "vendor/bin/phpstan analyse -l 6 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
|
||||
"phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins",
|
||||
"test": "vendor/bin/codecept run unit",
|
||||
"test-windows": "vendor\\bin\\codecept run unit"
|
||||
"test": "php -d register_argc_argv=On vendor/bin/codecept run unit",
|
||||
"test-windows": "php -d register_argc_argv=On vendor\\bin\\codecept run unit"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
||||
2845
composer.lock
generated
2845
composer.lock
generated
File diff suppressed because it is too large
Load Diff
81
index.php
81
index.php
@@ -10,7 +10,8 @@
|
||||
namespace Grav;
|
||||
|
||||
\define('GRAV_REQUEST_TIME', microtime(true));
|
||||
\define('GRAV_PHP_MIN', '8.2.0');
|
||||
|
||||
\define('GRAV_PHP_MIN', '8.3.0');
|
||||
|
||||
if (PHP_SAPI === 'cli-server') {
|
||||
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
|
||||
@@ -20,6 +21,46 @@ if (PHP_SAPI === 'cli-server') {
|
||||
}
|
||||
}
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
if (!isset($_SERVER['argv']) && !ini_get('register_argc_argv')) {
|
||||
$queryString = $_SERVER['QUERY_STRING'] ?? '';
|
||||
$_SERVER['argv'] = $queryString !== '' ? [$queryString] : [];
|
||||
$_SERVER['argc'] = $queryString !== '' ? 1 : 0;
|
||||
}
|
||||
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$path = parse_url($requestUri, PHP_URL_PATH) ?? '/';
|
||||
$path = str_replace('\\', '/', $path);
|
||||
|
||||
$scriptDir = str_replace('\\', '/', dirname($scriptName));
|
||||
if ($scriptDir && $scriptDir !== '/' && $scriptDir !== '.') {
|
||||
if (strpos($path, $scriptDir) === 0) {
|
||||
$path = substr($path, strlen($scriptDir));
|
||||
$path = $path === '' ? '/' : $path;
|
||||
}
|
||||
}
|
||||
|
||||
if ($path === '/___safe-upgrade-status') {
|
||||
$statusEndpoint = __DIR__ . '/user/plugins/admin/safe-upgrade-status.php';
|
||||
if (!\defined('GRAV_ROOT')) {
|
||||
// Minimal bootstrap so the status script has the expected constants.
|
||||
require_once __DIR__ . '/system/defines.php';
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (is_file($statusEndpoint)) {
|
||||
require $statusEndpoint;
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Safe upgrade status endpoint unavailable.',
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure vendor libraries exist
|
||||
$autoload = __DIR__ . '/vendor/autoload.php';
|
||||
if (!is_file($autoload)) {
|
||||
@@ -36,16 +77,50 @@ date_default_timezone_set(@date_default_timezone_get());
|
||||
@ini_set('default_charset', 'UTF-8');
|
||||
mb_internal_encoding('UTF-8');
|
||||
|
||||
// Use getcwd() for paths to support symlinked index.php (GRAV_ROOT uses getcwd())
|
||||
$gravRoot = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', getenv('GRAV_ROOT') ?: getcwd()), '/');
|
||||
|
||||
// Helper function to check if recovery mode is enabled in config (updates.recovery_mode)
|
||||
$isRecoveryEnabled = static function () use ($gravRoot) {
|
||||
$userConfig = $gravRoot . '/user/config/system.yaml';
|
||||
if (!is_file($userConfig)) {
|
||||
return true; // Default enabled
|
||||
}
|
||||
$content = file_get_contents($userConfig);
|
||||
if ($content === false) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('/^\s*updates:\s*\n(?:\s+\w+:.*\n)*?\s+recovery_mode:\s*(true|false|1|0)\s*$/m', $content, $matches)) {
|
||||
return in_array(strtolower($matches[1]), ['true', '1'], true);
|
||||
}
|
||||
return true; // Default enabled
|
||||
};
|
||||
|
||||
$recoveryFlag = $gravRoot . '/user/data/recovery.flag';
|
||||
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag) && $isRecoveryEnabled()) {
|
||||
if (!defined('GRAV_ROOT')) {
|
||||
define('GRAV_ROOT', $gravRoot);
|
||||
}
|
||||
require __DIR__ . '/system/recovery.php';
|
||||
return 0;
|
||||
}
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
// Get the Grav instance
|
||||
$grav = Grav::instance(array('loader' => $loader));
|
||||
$grav = Grav::instance(['loader' => $loader]);
|
||||
|
||||
// Process the page
|
||||
try {
|
||||
$grav->process();
|
||||
} catch (\Error|\Exception $e) {
|
||||
$grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
|
||||
$grav->fireEvent('onFatalException', new Event(['exception' => $e]));
|
||||
|
||||
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag) && $isRecoveryEnabled()) {
|
||||
require __DIR__ . '/system/recovery.php';
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
505
needs_fixing.txt
Normal file
505
needs_fixing.txt
Normal file
@@ -0,0 +1,505 @@
|
||||
------ ----------------------------------------------------
|
||||
Line src/Grav/Common/GPM/Response.php
|
||||
------ ----------------------------------------------------
|
||||
3 Class Grav\Common\GPM\Response not found.
|
||||
🪪 class.notFound
|
||||
💡 Learn more at
|
||||
https://phpstan.org/user-guide/discovering-symbols
|
||||
------ ----------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Common/Grav.php
|
||||
------ -----------------------------------------------------------
|
||||
148 No error to ignore is reported on line 148.
|
||||
681 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Common/Page/Collection.php
|
||||
------ -----------------------------------------------------------
|
||||
112 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
209 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Common/Processors/InitializeProcessor.php
|
||||
------ -----------------------------------------------------------
|
||||
58 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -------------------------------------------------------
|
||||
Line src/Grav/Common/Scheduler/Job.php
|
||||
------ -------------------------------------------------------
|
||||
574 Call to static method sendEmail() on an unknown class
|
||||
Grav\Plugin\Email\Utils.
|
||||
🪪 class.notFound
|
||||
💡 Learn more at
|
||||
https://phpstan.org/user-guide/discovering-symbols
|
||||
------ -------------------------------------------------------
|
||||
|
||||
------ ----------------------------------------------------------
|
||||
Line src/Grav/Common/Scheduler/SchedulerController.php
|
||||
------ ----------------------------------------------------------
|
||||
41 Class Grav\Common\Scheduler\ModernScheduler not found.
|
||||
🪪 class.notFound
|
||||
💡 Learn more at
|
||||
https://phpstan.org/user-guide/discovering-symbols
|
||||
45 Instantiated class Grav\Common\Scheduler\ModernScheduler
|
||||
not found.
|
||||
🪪 class.notFound
|
||||
💡 Learn more at
|
||||
https://phpstan.org/user-guide/discovering-symbols
|
||||
------ ----------------------------------------------------------
|
||||
|
||||
------ --------------------------------------------------------
|
||||
Line src/Grav/Common/Service/SchedulerServiceProvider.php
|
||||
------ --------------------------------------------------------
|
||||
55 Instantiated class Grav\Common\Scheduler\JobWorker not
|
||||
found.
|
||||
🪪 class.notFound
|
||||
💡 Learn more at
|
||||
https://phpstan.org/user-guide/discovering-symbols
|
||||
------ --------------------------------------------------------
|
||||
|
||||
------ ---------------------------------------------
|
||||
Line src/Grav/Common/Session.php
|
||||
------ ---------------------------------------------
|
||||
132 No error to ignore is reported on line 132.
|
||||
137 No error to ignore is reported on line 137.
|
||||
------ ---------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Common/Uri.php
|
||||
------ -----------------------------------------------------------
|
||||
1131 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ --------------------------------------------------------
|
||||
Line src/Grav/Console/Application/Application.php
|
||||
------ --------------------------------------------------------
|
||||
125 Return type mixed of method
|
||||
Grav\Console\Application\Application::configureIO() is
|
||||
not covariant with return type void of method
|
||||
Symfony\Component\Console\Application::configureIO().
|
||||
------ --------------------------------------------------------
|
||||
|
||||
------ ---------------------------------------------------------
|
||||
Line src/Grav/Console/ConsoleCommand.php
|
||||
------ ---------------------------------------------------------
|
||||
29 Return type mixed of method
|
||||
Grav\Console\ConsoleCommand::execute() is not covariant
|
||||
with return type int of method
|
||||
Symfony\Component\Console\Command\Command::execute().
|
||||
------ ---------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Console/ConsoleTrait.php (in context of class
|
||||
Grav\Console\ConsoleCommand)
|
||||
------ -----------------------------------------------------------
|
||||
89 Method Grav\Console\ConsoleCommand::addOption() overrides
|
||||
method
|
||||
Symfony\Component\Console\Command\Command::addOption()
|
||||
but misses parameter #6 $suggestedValues.
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ --------------------------------------------------------
|
||||
Line src/Grav/Console/ConsoleTrait.php (in context of class
|
||||
Grav\Console\GpmCommand)
|
||||
------ --------------------------------------------------------
|
||||
89 Method Grav\Console\GpmCommand::addOption() overrides
|
||||
method
|
||||
Symfony\Component\Console\Command\Command::addOption()
|
||||
but misses parameter #6 $suggestedValues.
|
||||
------ --------------------------------------------------------
|
||||
|
||||
------ --------------------------------------------------------
|
||||
Line src/Grav/Console/ConsoleTrait.php (in context of class
|
||||
Grav\Console\GravCommand)
|
||||
------ --------------------------------------------------------
|
||||
89 Method Grav\Console\GravCommand::addOption() overrides
|
||||
method
|
||||
Symfony\Component\Console\Command\Command::addOption()
|
||||
but misses parameter #6 $suggestedValues.
|
||||
------ --------------------------------------------------------
|
||||
|
||||
------ ----------------------------------------------------------
|
||||
Line src/Grav/Console/GpmCommand.php
|
||||
------ ----------------------------------------------------------
|
||||
31 Return type mixed of method
|
||||
Grav\Console\GpmCommand::execute() is not covariant with
|
||||
return type int of method
|
||||
Symfony\Component\Console\Command\Command::execute().
|
||||
39 No error to ignore is reported on line 39.
|
||||
------ ----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Console/GravCommand.php
|
||||
------ -----------------------------------------------------------
|
||||
29 Return type mixed of method
|
||||
Grav\Console\GravCommand::execute() is not covariant with
|
||||
return type int of method
|
||||
Symfony\Component\Console\Command\Command::execute().
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Acl/RecursiveActionIterator.php
|
||||
------ -----------------------------------------------------------
|
||||
62 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ ----------------------------------------------------------
|
||||
Line src/Grav/Framework/Cache/CacheTrait.php (in context of
|
||||
class Grav\Framework\Cache\AbstractCache)
|
||||
------ ----------------------------------------------------------
|
||||
87 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::get() is not
|
||||
covariant with return type mixed of method
|
||||
Psr\SimpleCache\CacheInterface::get().
|
||||
102 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::set() is not
|
||||
covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::set().
|
||||
117 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::delete() is not
|
||||
covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::delete().
|
||||
127 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::clear() is not
|
||||
covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::clear().
|
||||
138 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::getMultiple() is not
|
||||
covariant with return type iterable of method
|
||||
Psr\SimpleCache\CacheInterface::getMultiple().
|
||||
181 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::setMultiple() is not
|
||||
covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::setMultiple().
|
||||
214 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::deleteMultiple() is
|
||||
not covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::deleteMultiple().
|
||||
242 Return type mixed of method
|
||||
Grav\Framework\Cache\AbstractCache::has() is not
|
||||
covariant with return type bool of method
|
||||
Psr\SimpleCache\CacheInterface::has().
|
||||
------ ----------------------------------------------------------
|
||||
|
||||
------ ----------------------------------------------------------
|
||||
Line src/Grav/Framework/Collection/AbstractFileCollection.php
|
||||
------ ----------------------------------------------------------
|
||||
95 No error to ignore is reported on line 95.
|
||||
------ ----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Collection/AbstractIndexCollection.php
|
||||
------ -----------------------------------------------------------
|
||||
154 No error to ignore is reported on line 154.
|
||||
168 No error to ignore is reported on line 168.
|
||||
185 No error to ignore is reported on line 185.
|
||||
201 No error to ignore is reported on line 201.
|
||||
507 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Collection/AbstractLazyCollection.php
|
||||
------ -----------------------------------------------------------
|
||||
29 Property
|
||||
Grav\Framework\Collection\AbstractLazyCollection::$collec
|
||||
tion overriding property
|
||||
Doctrine\Common\Collections\AbstractLazyCollection<TKey o
|
||||
f (int|string),T>::$collection (Doctrine\Common\Collectio
|
||||
ns\Collection|null) should also have native type
|
||||
Doctrine\Common\Collections\Collection|null.
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Contracts/Relationships/RelationshipIn
|
||||
terface.php
|
||||
------ -----------------------------------------------------------
|
||||
80 Return type iterable of method
|
||||
Grav\Framework\Contracts\Relationships\RelationshipInterf
|
||||
ace::getIterator() is not covariant with tentative return
|
||||
type Traversable of method IteratorAggregate<string,T of
|
||||
Grav\Framework\Contracts\Object\IdentifierInterface>::get
|
||||
Iterator().
|
||||
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
|
||||
attribute to temporarily suppress the error.
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Filesystem/Filesystem.php
|
||||
------ -----------------------------------------------------------
|
||||
51 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
252 No error to ignore is reported on line 252.
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ ---------------------------------------------
|
||||
Line src/Grav/Framework/Flex/FlexCollection.php
|
||||
------ ---------------------------------------------
|
||||
102 No error to ignore is reported on line 102.
|
||||
------ ---------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Flex/FlexIdentifier.php
|
||||
------ -----------------------------------------------------------
|
||||
27 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ ----------------------------------------------------------
|
||||
Line src/Grav/Framework/Flex/FlexIndex.php
|
||||
------ ----------------------------------------------------------
|
||||
109 No error to ignore is reported on line 109.
|
||||
934 Method Grav\Framework\Flex\FlexIndex::reduce() should
|
||||
return TInitial|TReturn but return statement is missing.
|
||||
🪪 return.missing
|
||||
------ ----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Form/FormFlashFile.php
|
||||
------ -----------------------------------------------------------
|
||||
62 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::getStream() is not
|
||||
covariant with return type
|
||||
Psr\Http\Message\StreamInterface of method
|
||||
Psr\Http\Message\UploadedFileInterface::getStream().
|
||||
83 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::moveTo() is not
|
||||
covariant with return type void of method
|
||||
Psr\Http\Message\UploadedFileInterface::moveTo().
|
||||
123 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::getSize() is not
|
||||
covariant with return type int|null of method
|
||||
Psr\Http\Message\UploadedFileInterface::getSize().
|
||||
131 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::getError() is not
|
||||
covariant with return type int of method
|
||||
Psr\Http\Message\UploadedFileInterface::getError().
|
||||
139 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::getClientFilename() is
|
||||
not covariant with return type string|null of method
|
||||
Psr\Http\Message\UploadedFileInterface::getClientFilename
|
||||
().
|
||||
147 Return type mixed of method
|
||||
Grav\Framework\Form\FormFlashFile::getClientMediaType()
|
||||
is not covariant with return type string|null of method
|
||||
Psr\Http\Message\UploadedFileInterface::getClientMediaTyp
|
||||
e().
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Logger/Processors/UserProcessor.php
|
||||
------ -----------------------------------------------------------
|
||||
24 Parameter #1 $record (array) of method
|
||||
Grav\Framework\Logger\Processors\UserProcessor::__invoke(
|
||||
) is not contravariant with parameter #1 $record
|
||||
(Monolog\LogRecord) of method
|
||||
Monolog\Processor\ProcessorInterface::__invoke().
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Media/MediaIdentifier.php
|
||||
------ -----------------------------------------------------------
|
||||
30 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Media/UploadedMediaObject.php
|
||||
------ -----------------------------------------------------------
|
||||
36 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Mime/MimeTypes.php
|
||||
------ -----------------------------------------------------------
|
||||
42 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ ------------------------------------------------
|
||||
Line src/Grav/Framework/Object/ObjectCollection.php
|
||||
------ ------------------------------------------------
|
||||
96 No error to ignore is reported on line 96.
|
||||
------ ------------------------------------------------
|
||||
|
||||
------ ---------------------------------------------
|
||||
Line src/Grav/Framework/Object/ObjectIndex.php
|
||||
------ ---------------------------------------------
|
||||
193 No error to ignore is reported on line 193.
|
||||
------ ---------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Psr7/Stream.php
|
||||
------ -----------------------------------------------------------
|
||||
31 Unsafe usage of new static().
|
||||
🪪 new.static
|
||||
💡 See:
|
||||
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
|
||||
ge-of-new-static
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrai
|
||||
t.php (in context of class
|
||||
Grav\Framework\Psr7\ServerRequest)
|
||||
------ -----------------------------------------------------------
|
||||
51 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::getAttributes() is not
|
||||
covariant with return type array of method
|
||||
Psr\Http\Message\ServerRequestInterface::getAttributes().
|
||||
60 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::getCookieParams() is
|
||||
not covariant with return type array of method
|
||||
Psr\Http\Message\ServerRequestInterface::getCookieParams(
|
||||
).
|
||||
76 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::getQueryParams() is
|
||||
not covariant with return type array of method
|
||||
Psr\Http\Message\ServerRequestInterface::getQueryParams()
|
||||
.
|
||||
84 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::getServerParams() is
|
||||
not covariant with return type array of method
|
||||
Psr\Http\Message\ServerRequestInterface::getServerParams(
|
||||
).
|
||||
92 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::getUploadedFiles() is
|
||||
not covariant with return type array of method
|
||||
Psr\Http\Message\ServerRequestInterface::getUploadedFiles
|
||||
().
|
||||
100 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withAttribute() is not
|
||||
covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withAttribute().
|
||||
125 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withoutAttribute() is
|
||||
not covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withoutAttribute
|
||||
().
|
||||
136 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withCookieParams() is
|
||||
not covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withCookieParams
|
||||
().
|
||||
147 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withParsedBody() is
|
||||
not covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withParsedBody()
|
||||
.
|
||||
158 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withQueryParams() is
|
||||
not covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withQueryParams(
|
||||
).
|
||||
169 Return type mixed of method
|
||||
Grav\Framework\Psr7\ServerRequest::withUploadedFiles() is
|
||||
not covariant with return type
|
||||
Psr\Http\Message\ServerRequestInterface of method
|
||||
Psr\Http\Message\ServerRequestInterface::withUploadedFile
|
||||
s().
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Grav/Framework/Relationships/Relationships.php
|
||||
------ -----------------------------------------------------------
|
||||
107 Return type mixed of method
|
||||
Grav\Framework\Relationships\Relationships::offsetSet()
|
||||
is not covariant with tentative return type void of
|
||||
method
|
||||
ArrayAccess<string,Grav\Framework\Contracts\Relationships
|
||||
\RelationshipInterface<T of Grav\Framework\Contracts\Obje
|
||||
ct\IdentifierInterface, P of
|
||||
Grav\Framework\Contracts\Object\IdentifierInterface>>::of
|
||||
fsetSet().
|
||||
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
|
||||
attribute to temporarily suppress the error.
|
||||
116 Return type mixed of method
|
||||
Grav\Framework\Relationships\Relationships::offsetUnset()
|
||||
is not covariant with tentative return type void of
|
||||
method
|
||||
ArrayAccess<string,Grav\Framework\Contracts\Relationships
|
||||
\RelationshipInterface<T of Grav\Framework\Contracts\Obje
|
||||
ct\IdentifierInterface, P of
|
||||
Grav\Framework\Contracts\Object\IdentifierInterface>>::of
|
||||
fsetUnset().
|
||||
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
|
||||
attribute to temporarily suppress the error.
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
------ -----------------------------------------------------------
|
||||
Line src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php
|
||||
------ -----------------------------------------------------------
|
||||
30 Parameter #1 $node (Twig_NodeInterface) of method
|
||||
Twig\DeferredExtension\DeferredNodeVisitorCompat::enterNo
|
||||
de() is not contravariant with parameter #1 $node
|
||||
(Twig\Node\Node) of method
|
||||
Twig\NodeVisitor\NodeVisitorInterface::enterNode().
|
||||
30 Parameter $node of method
|
||||
Twig\DeferredExtension\DeferredNodeVisitorCompat::enterNo
|
||||
de() has invalid type Twig_NodeInterface.
|
||||
🪪 class.notFound
|
||||
46 Parameter #1 $node (Twig_NodeInterface) of method
|
||||
Twig\DeferredExtension\DeferredNodeVisitorCompat::leaveNo
|
||||
de() is not contravariant with parameter #1 $node
|
||||
(Twig\Node\Node) of method
|
||||
Twig\NodeVisitor\NodeVisitorInterface::leaveNode().
|
||||
46 Parameter $node of method
|
||||
Twig\DeferredExtension\DeferredNodeVisitorCompat::leaveNo
|
||||
de() has invalid type Twig_NodeInterface.
|
||||
🪪 class.notFound
|
||||
------ -----------------------------------------------------------
|
||||
|
||||
[ERROR] Found 74 errors
|
||||
|
||||
@@ -1,67 +1,5 @@
|
||||
div.phpdebugbar {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
div.phpdebugbar a.phpdebugbar-restore-btn::after {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==) !important;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.phpdebugbar pre {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.phpdebugbar div.phpdebugbar-header > div > * {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.phpdebugbar a.phpdebugbar-restore-btn {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==);
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
|
||||
background: #3DB9EC;
|
||||
color: #fff;
|
||||
margin-top: -1px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.phpdebugbar .phpdebugbar-widgets-toolbar {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-left: 5px;
|
||||
padding-right: 2px;
|
||||
padding-top: 2px;
|
||||
background-color: #fafafa !important;
|
||||
width: auto !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.phpdebugbar .phpdebugbar-widgets-toolbar input {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.phpdebugbar input[type=text] {
|
||||
padding: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
|
||||
font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
|
||||
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.phpdebugbar pre, .phpdebugbar code {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -122,4 +122,13 @@ form:
|
||||
default: '* 3 * * *'
|
||||
validate:
|
||||
required: true
|
||||
.schedule_environment:
|
||||
type: select
|
||||
label: PLUGIN_ADMIN.BACKUPS_PROFILE_ENVIRONMENT
|
||||
help: PLUGIN_ADMIN.BACKUPS_PROFILE_ENVIRONMENT_HELP
|
||||
default: ''
|
||||
options:
|
||||
'': 'Default (cli)'
|
||||
localhost: 'Localhost'
|
||||
cli: 'CLI'
|
||||
|
||||
|
||||
@@ -4,74 +4,788 @@ form:
|
||||
validation: loose
|
||||
|
||||
fields:
|
||||
scheduler_tabs:
|
||||
type: tabs
|
||||
active: 1
|
||||
|
||||
status_title:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.SCHEDULER_STATUS
|
||||
underline: true
|
||||
fields:
|
||||
status_tab:
|
||||
type: tab
|
||||
title: PLUGIN_ADMIN.SCHEDULER_STATUS
|
||||
|
||||
status:
|
||||
type: cronstatus
|
||||
validate:
|
||||
type: commalist
|
||||
fields:
|
||||
status_title:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.SCHEDULER_STATUS
|
||||
underline: true
|
||||
|
||||
jobs_title:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.SCHEDULER_JOBS
|
||||
underline: true
|
||||
status:
|
||||
type: cronstatus
|
||||
validate:
|
||||
type: commalist
|
||||
|
||||
webhook_status_override:
|
||||
type: display
|
||||
label:
|
||||
content: |
|
||||
<script>
|
||||
(function() {
|
||||
function updateSchedulerStatus() {
|
||||
// Find all notice bars
|
||||
var notices = document.querySelectorAll('.notice');
|
||||
var webhookStatusChecked = false;
|
||||
|
||||
// Check for modern scheduler and webhook settings
|
||||
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.webhook_enabled) {
|
||||
notices.forEach(function(notice) {
|
||||
if (notice.textContent.includes('Not Enabled for user:')) {
|
||||
// This is the cron status notice - replace it
|
||||
notice.className = 'notice info';
|
||||
notice.innerHTML = '<i class="fa fa-fw fa-check-circle"></i> <strong>Webhook Active</strong> - Scheduler can be triggered via webhook. Cron is not configured.';
|
||||
}
|
||||
});
|
||||
|
||||
// Also update the main status if it exists
|
||||
var statusDiv = document.querySelector('.cronstatus-status');
|
||||
if (statusDiv && statusDiv.textContent.includes('Not Enabled')) {
|
||||
statusDiv.className = 'cronstatus-status success';
|
||||
statusDiv.innerHTML = '<i class="fa fa-fw fa-check"></i> Webhook Ready';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Webhook status check failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Run on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', updateSchedulerStatus);
|
||||
} else {
|
||||
updateSchedulerStatus();
|
||||
}
|
||||
|
||||
// Also run after a short delay to catch any late-rendered elements
|
||||
setTimeout(updateSchedulerStatus, 500);
|
||||
})();
|
||||
</script>
|
||||
markdown: false
|
||||
|
||||
status_enhanced:
|
||||
type: display
|
||||
label:
|
||||
content: |
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if webhook is enabled
|
||||
var webhookEnabled = document.querySelector('[name="data[scheduler][modern][webhook][enabled]"]:checked');
|
||||
var statusDiv = document.querySelector('.cronstatus-status');
|
||||
|
||||
// Also find the parent notice bar
|
||||
var noticeBar = document.querySelector('.notice.alert');
|
||||
|
||||
if (statusDiv) {
|
||||
var currentStatus = statusDiv.textContent || statusDiv.innerText;
|
||||
var cronReady = currentStatus.includes('Ready');
|
||||
var cronNotEnabled = currentStatus.includes('Not Enabled');
|
||||
|
||||
// Check if scheduler-webhook plugin exists
|
||||
var webhookPluginInstalled = false;
|
||||
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
webhookPluginInstalled = true;
|
||||
updateStatusDisplay(data);
|
||||
})
|
||||
.catch(error => {
|
||||
updateStatusDisplay(null);
|
||||
});
|
||||
|
||||
function updateStatusDisplay(healthData) {
|
||||
var isWebhookEnabled = webhookEnabled && webhookEnabled.value == '1';
|
||||
var isWebhookReady = webhookPluginInstalled && isWebhookEnabled && healthData && healthData.webhook_enabled;
|
||||
|
||||
// Update the main status text
|
||||
var mainStatusText = '';
|
||||
var mainStatusClass = '';
|
||||
|
||||
if (cronReady && isWebhookReady) {
|
||||
mainStatusText = 'Cron and Webhook Ready';
|
||||
mainStatusClass = 'success';
|
||||
} else if (cronReady) {
|
||||
mainStatusText = 'Cron Ready';
|
||||
mainStatusClass = 'success';
|
||||
} else if (isWebhookReady) {
|
||||
mainStatusText = 'Webhook Ready (No Cron)';
|
||||
mainStatusClass = 'success'; // Changed from warning to success
|
||||
} else if (cronNotEnabled && !isWebhookReady) {
|
||||
mainStatusText = 'Not Configured';
|
||||
mainStatusClass = 'error';
|
||||
} else {
|
||||
mainStatusText = 'Configuration Pending';
|
||||
mainStatusClass = 'warning';
|
||||
}
|
||||
|
||||
// Update the notice bar if webhooks are ready
|
||||
if (noticeBar && isWebhookReady) {
|
||||
// Change from error (red) to success (green) or info (blue)
|
||||
noticeBar.classList.remove('alert');
|
||||
noticeBar.classList.add('info');
|
||||
|
||||
var noticeIcon = noticeBar.querySelector('i.fa');
|
||||
if (noticeIcon) {
|
||||
noticeIcon.classList.remove('fa-times-circle');
|
||||
noticeIcon.classList.add('fa-check-circle');
|
||||
}
|
||||
|
||||
var noticeText = noticeBar.querySelector('strong') || noticeBar;
|
||||
var username = noticeText.textContent.match(/user:\s*(\w+)/);
|
||||
if (username) {
|
||||
noticeText.innerHTML = 'Webhook Ready for user: <b>' + username[1] + '</b> (Cron not configured)';
|
||||
} else {
|
||||
noticeText.innerHTML = mainStatusText;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the main status div
|
||||
if (statusDiv) {
|
||||
statusDiv.innerHTML = '<i class="fa fa-fw fa-' +
|
||||
(mainStatusClass === 'success' ? 'check' : mainStatusClass === 'warning' ? 'exclamation' : 'times') +
|
||||
'"></i> ' + mainStatusText;
|
||||
statusDiv.className = 'cronstatus-status ' + mainStatusClass;
|
||||
}
|
||||
|
||||
// Update install instructions button/content
|
||||
var installButton = document.querySelector('.cronstatus-install-button');
|
||||
var installDiv = document.querySelector('.cronstatus-install');
|
||||
|
||||
if (installDiv) {
|
||||
var installHtml = '<div class="alert alert-info">';
|
||||
installHtml += '<h4>Setup Instructions:</h4>';
|
||||
|
||||
var hasInstructions = false;
|
||||
|
||||
// Cron setup
|
||||
if (!cronReady) {
|
||||
installHtml += '<p><strong>Option 1: Traditional Cron</strong><br>';
|
||||
installHtml += 'Run: <code>bin/grav scheduler --install</code><br>';
|
||||
installHtml += 'This will add a cron job that runs every minute.</p>';
|
||||
hasInstructions = true;
|
||||
}
|
||||
|
||||
// Webhook setup
|
||||
if (!webhookPluginInstalled) {
|
||||
installHtml += '<p><strong>Option 2: Webhook Support</strong><br>';
|
||||
installHtml += '1. Install plugin: <code>bin/gpm install scheduler-webhook</code><br>';
|
||||
installHtml += '2. Configure webhook token in Advanced Features tab<br>';
|
||||
installHtml += '3. Use webhook URL in your CI/CD or cloud scheduler</p>';
|
||||
hasInstructions = true;
|
||||
} else if (!isWebhookEnabled) {
|
||||
installHtml += '<p><strong>Webhook Plugin Installed</strong><br>';
|
||||
installHtml += 'Enable webhooks in Advanced Features tab and set a secure token.</p>';
|
||||
hasInstructions = true;
|
||||
} else if (isWebhookReady) {
|
||||
installHtml += '<p><strong>✅ Webhook is Active!</strong><br>';
|
||||
installHtml += 'Trigger URL: <code>' + window.location.origin + '/grav-editor-pro/scheduler/webhook</code><br>';
|
||||
installHtml += 'Use with Authorization header: <code>Bearer YOUR_TOKEN</code></p>';
|
||||
|
||||
if (!cronReady) {
|
||||
installHtml += '<p class="text-muted"><small>Note: No cron job configured. Scheduler runs only via webhook triggers.</small></p>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInstructions && cronReady) {
|
||||
installHtml += '<p><strong>✅ Cron is configured and ready!</strong><br>';
|
||||
installHtml += 'The scheduler runs automatically every minute via system cron.</p>';
|
||||
|
||||
}
|
||||
|
||||
installHtml += '</div>';
|
||||
installDiv.innerHTML = installHtml;
|
||||
|
||||
// Update button text based on status
|
||||
if (installButton) {
|
||||
if (cronReady && isWebhookReady) {
|
||||
installButton.innerHTML = '<i class="fa fa-info-circle"></i> Configuration Details';
|
||||
} else if (cronReady || isWebhookReady) {
|
||||
installButton.innerHTML = '<i class="fa fa-plus-circle"></i> Add More Triggers';
|
||||
} else {
|
||||
installButton.innerHTML = '<i class="fa fa-exclamation-triangle"></i> Install Instructions';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
custom_jobs:
|
||||
type: list
|
||||
style: vertical
|
||||
label:
|
||||
classes: cron-job-list compact
|
||||
key: id
|
||||
fields:
|
||||
.id:
|
||||
type: key
|
||||
label: ID
|
||||
placeholder: 'process-name'
|
||||
validate:
|
||||
required: true
|
||||
pattern: '[a-zа-я0-9_\-]+'
|
||||
max: 20
|
||||
message: 'ID must be lowercase with dashes/underscores only and less than 20 characters'
|
||||
.command:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.COMMAND
|
||||
placeholder: 'ls'
|
||||
validate:
|
||||
required: true
|
||||
.args:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
|
||||
placeholder: '-lah'
|
||||
.at:
|
||||
type: text
|
||||
wrapper_classes: cron-selector
|
||||
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
|
||||
help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
|
||||
placeholder: '* * * * *'
|
||||
validate:
|
||||
required: true
|
||||
.output:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT
|
||||
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP
|
||||
placeholder: 'logs/ls-cron.out'
|
||||
.output_mode:
|
||||
type: select
|
||||
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE
|
||||
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP
|
||||
default: append
|
||||
options:
|
||||
append: Append
|
||||
overwrite: Overwrite
|
||||
.email:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.SCHEDULER_EMAIL
|
||||
help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP
|
||||
placeholder: 'notifications@yoursite.com'
|
||||
modern_health:
|
||||
type: display
|
||||
label: Health Status
|
||||
content: |
|
||||
<div id="scheduler-health-status">
|
||||
<div class="text-muted">Checking health...</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
function loadHealthStatus() {
|
||||
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
var statusEl = document.getElementById('scheduler-health-status');
|
||||
if (!statusEl) return;
|
||||
|
||||
// Modern card-based layout
|
||||
var statusColor = '#6c757d';
|
||||
var statusLabel = data.status || 'unknown';
|
||||
if (data.status === 'healthy') statusColor = '#28a745';
|
||||
else if (data.status === 'warning') statusColor = '#ffc107';
|
||||
else if (data.status === 'critical') statusColor = '#dc3545';
|
||||
|
||||
var html = '<div style="display: flex; flex-direction: column; gap: 1rem;">';
|
||||
|
||||
// Status card
|
||||
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); border-radius: 6px; border: 1px solid #e9ecef; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">';
|
||||
html += '<span style="font-weight: 500; color: #495057;">Status:</span>';
|
||||
html += '<span style="background: ' + statusColor + '; color: white; padding: 0.375rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.025em;">' + statusLabel + '</span>';
|
||||
html += '</div>';
|
||||
|
||||
// Info grid
|
||||
html += '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;">';
|
||||
|
||||
// Last run card
|
||||
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
|
||||
html += '<div style="color: #6c757d; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem;">Last Run</div>';
|
||||
if (data.last_run) {
|
||||
var age = data.last_run_age;
|
||||
var ageText = 'just now';
|
||||
if (age > 86400) {
|
||||
ageText = Math.floor(age / 86400) + ' day(s) ago';
|
||||
} else if (age > 3600) {
|
||||
ageText = Math.floor(age / 3600) + ' hour(s) ago';
|
||||
} else if (age > 60) {
|
||||
ageText = Math.floor(age / 60) + ' minute(s) ago';
|
||||
} else if (age > 0) {
|
||||
ageText = age + ' second(s) ago';
|
||||
}
|
||||
html += '<div style="font-size: 1rem; color: #212529; font-weight: 500;">' + ageText + '</div>';
|
||||
} else {
|
||||
html += '<div style="font-size: 1rem; color: #6c757d;">Never</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Jobs count card
|
||||
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
|
||||
html += '<div style="color: #6c757d; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem;">Scheduled Jobs</div>';
|
||||
html += '<div style="font-size: 1rem; color: #212529; font-weight: 500;">' + (data.scheduled_jobs || 0) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>'; // Close grid
|
||||
|
||||
// Additional info if available
|
||||
if (data.modern_features && data.queue_size !== undefined) {
|
||||
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
|
||||
html += '<span style="color: #6c757d; font-size: 0.875rem;">Queue Size: </span>';
|
||||
html += '<span style="font-weight: 500;">' + data.queue_size + '</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Failed jobs warning
|
||||
if (data.failed_jobs_24h > 0) {
|
||||
html += '<div style="background: #fff5f5; border: 1px solid #feb2b2; border-radius: 6px; padding: 0.75rem; color: #c53030;">';
|
||||
html += '<strong>⚠️ Failed Jobs (24h):</strong> ' + data.failed_jobs_24h;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>'; // Close main container
|
||||
statusEl.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
var statusEl = document.getElementById('scheduler-health-status');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '<div class="alert alert-warning">Unable to fetch health status. Ensure scheduler-webhook plugin is installed.</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load on page ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadHealthStatus);
|
||||
} else {
|
||||
loadHealthStatus();
|
||||
}
|
||||
|
||||
// Refresh every 30 seconds
|
||||
setInterval(loadHealthStatus, 30000);
|
||||
})();
|
||||
</script>
|
||||
markdown: false
|
||||
|
||||
trigger_methods:
|
||||
type: display
|
||||
label: Active Triggers
|
||||
content: |
|
||||
<div id="scheduler-triggers">
|
||||
<div class="text-muted">Checking triggers...</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
function loadTriggers() {
|
||||
// Check cron status from the main status field
|
||||
var cronReady = false;
|
||||
var statusDiv = document.querySelector('.cronstatus-status');
|
||||
if (statusDiv) {
|
||||
var statusText = statusDiv.textContent || statusDiv.innerText;
|
||||
cronReady = statusText.includes('Ready');
|
||||
}
|
||||
|
||||
// Check webhook status
|
||||
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
var triggersEl = document.getElementById('scheduler-triggers');
|
||||
if (!triggersEl) return;
|
||||
|
||||
var html = '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
||||
|
||||
// Cron trigger card
|
||||
var cronIcon = cronReady ? '✅' : '❌';
|
||||
var cronStatus = cronReady ? 'Active' : 'Not Configured';
|
||||
var cronStatusColor = cronReady ? '#28a745' : '#6c757d';
|
||||
var cardBg = cronReady ? '#f8f9fa' : '#fff';
|
||||
|
||||
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: ' + cardBg + '; border: 1px solid #e9ecef; border-radius: 4px;">';
|
||||
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
|
||||
html += '<span style="font-size: 1.25rem; line-height: 1;">' + cronIcon + '</span>';
|
||||
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Cron:</span>';
|
||||
html += '</div>';
|
||||
html += '<span style="background: ' + cronStatusColor + '; color: white; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">' + cronStatus + '</span>';
|
||||
html += '</div>';
|
||||
|
||||
// Webhook trigger card
|
||||
if (data.webhook_enabled) {
|
||||
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px;">';
|
||||
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
|
||||
html += '<span style="font-size: 1.25rem; line-height: 1;">✅</span>';
|
||||
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Webhook:</span>';
|
||||
html += '</div>';
|
||||
html += '<span style="background: #28a745; color: white; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">ACTIVE</span>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
// Show webhook as not configured/disabled
|
||||
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #fff; border: 1px solid #e9ecef; border-radius: 4px;">';
|
||||
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
|
||||
html += '<span style="font-size: 1.25rem; line-height: 1;">⚠️</span>';
|
||||
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Webhook:</span>';
|
||||
html += '</div>';
|
||||
html += '<span style="background: #ffc107; color: #212529; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">DISABLED</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Add warning if no triggers active
|
||||
if (!cronReady && !data.webhook_enabled) {
|
||||
html += '<div class="alert alert-warning" style="margin-top: 1rem;"><i class="fa fa-exclamation-triangle"></i> No triggers active! Configure cron or enable webhooks.</div>';
|
||||
}
|
||||
|
||||
triggersEl.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
var triggersEl = document.getElementById('scheduler-triggers');
|
||||
if (triggersEl) {
|
||||
// Show just cron status if health endpoint not available
|
||||
var html = '<ul class="list-unstyled">';
|
||||
if (cronReady) {
|
||||
html += '<li>✅ <strong>Cron:</strong> <span class="badge badge-success">Active</span></li>';
|
||||
} else {
|
||||
html += '<li>❌ <strong>Cron:</strong> <span class="badge badge-secondary">Not Configured</span></li>';
|
||||
}
|
||||
html += '<li>⚠️ <strong>Webhook:</strong> <span class="badge badge-secondary">Plugin Not Installed</span></li>';
|
||||
html += '</ul>';
|
||||
triggersEl.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load on page ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadTriggers);
|
||||
} else {
|
||||
loadTriggers();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
markdown: false
|
||||
|
||||
jobs_tab:
|
||||
type: tab
|
||||
title: PLUGIN_ADMIN.SCHEDULER_JOBS
|
||||
|
||||
fields:
|
||||
jobs_title:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.SCHEDULER_JOBS
|
||||
underline: true
|
||||
|
||||
custom_jobs:
|
||||
type: list
|
||||
style: vertical
|
||||
label:
|
||||
classes: cron-job-list compact
|
||||
key: id
|
||||
fields:
|
||||
.id:
|
||||
type: key
|
||||
label: ID
|
||||
placeholder: 'process-name'
|
||||
validate:
|
||||
required: true
|
||||
pattern: '[a-zа-я0-9_\-]+'
|
||||
max: 20
|
||||
message: 'ID must be lowercase with dashes/underscores only and less than 20 characters'
|
||||
.command:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.COMMAND
|
||||
placeholder: 'ls'
|
||||
validate:
|
||||
required: true
|
||||
.args:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
|
||||
placeholder: '-lah'
|
||||
.at:
|
||||
type: text
|
||||
wrapper_classes: cron-selector
|
||||
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
|
||||
help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
|
||||
placeholder: '* * * * *'
|
||||
validate:
|
||||
required: true
|
||||
.output:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT
|
||||
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP
|
||||
placeholder: 'logs/ls-cron.out'
|
||||
.output_mode:
|
||||
type: select
|
||||
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE
|
||||
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP
|
||||
default: append
|
||||
options:
|
||||
append: Append
|
||||
overwrite: Overwrite
|
||||
.email:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.SCHEDULER_EMAIL
|
||||
help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP
|
||||
placeholder: 'notifications@yoursite.com'
|
||||
|
||||
modern_tab:
|
||||
type: tab
|
||||
title: Advanced Features
|
||||
|
||||
fields:
|
||||
workers_section:
|
||||
type: section
|
||||
title: Worker Configuration
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
modern.workers:
|
||||
type: number
|
||||
label: Concurrent Workers
|
||||
help: Number of jobs that can run simultaneously (1 = sequential)
|
||||
default: 4
|
||||
size: x-small
|
||||
append: workers
|
||||
validate:
|
||||
type: int
|
||||
min: 1
|
||||
max: 10
|
||||
|
||||
retry_section:
|
||||
type: section
|
||||
title: Retry Configuration
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
modern.retry.enabled:
|
||||
type: toggle
|
||||
label: Enable Job Retry
|
||||
help: Automatically retry failed jobs
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
modern.retry.max_attempts:
|
||||
type: number
|
||||
label: Maximum Retry Attempts
|
||||
help: Maximum number of times to retry a failed job
|
||||
default: 3
|
||||
size: x-small
|
||||
append: retries
|
||||
validate:
|
||||
type: int
|
||||
min: 1
|
||||
max: 10
|
||||
|
||||
modern.retry.backoff:
|
||||
type: select
|
||||
label: Retry Backoff Strategy
|
||||
help: How to calculate delay between retries
|
||||
default: exponential
|
||||
options:
|
||||
linear: Linear (fixed delay)
|
||||
exponential: Exponential (increasing delay)
|
||||
|
||||
queue_section:
|
||||
type: section
|
||||
title: Queue Configuration
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
modern.queue.path:
|
||||
type: text
|
||||
label: Queue Storage Path
|
||||
help: Where to store queued jobs
|
||||
default: 'user-data://scheduler/queue'
|
||||
placeholder: 'user-data://scheduler/queue'
|
||||
|
||||
modern.queue.max_size:
|
||||
type: number
|
||||
label: Maximum Queue Size
|
||||
help: Maximum number of jobs that can be queued
|
||||
default: 1000
|
||||
size: x-small
|
||||
append: jobs
|
||||
validate:
|
||||
type: int
|
||||
min: 100
|
||||
max: 10000
|
||||
|
||||
history_section:
|
||||
type: section
|
||||
title: Job History
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
modern.history.enabled:
|
||||
type: toggle
|
||||
label: Enable Job History
|
||||
help: Track execution history for all jobs
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
modern.history.retention_days:
|
||||
type: number
|
||||
label: History Retention (days)
|
||||
help: How long to keep job history
|
||||
default: 30
|
||||
size: x-small
|
||||
append: days
|
||||
validate:
|
||||
type: int
|
||||
min: 1
|
||||
max: 365
|
||||
|
||||
webhook_section:
|
||||
type: section
|
||||
title: Webhook Configuration
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
webhook_plugin_status:
|
||||
type: webhook-status
|
||||
label:
|
||||
modern.webhook.enabled:
|
||||
type: toggle
|
||||
label: Enable Webhook Triggers
|
||||
help: Allow triggering scheduler via HTTP webhook
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
modern.webhook.token:
|
||||
type: text
|
||||
label: Webhook Security Token
|
||||
help: Secret token for authenticating webhook requests. Keep this secret!
|
||||
placeholder: 'Click Generate to create a secure token'
|
||||
autocomplete: 'off'
|
||||
|
||||
webhook_token_generate:
|
||||
type: display
|
||||
label:
|
||||
content: |
|
||||
<div style="margin-top: -10px; margin-bottom: 15px;">
|
||||
<button type="button" class="button button-primary" onclick="generateWebhookToken()">
|
||||
<i class="fa fa-refresh"></i> Generate Token
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
function generateWebhookToken() {
|
||||
try {
|
||||
// Generate token
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
const token = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Try multiple selectors to find the field
|
||||
let field = document.querySelector('[name="data[scheduler][modern][webhook][token]"]');
|
||||
if (!field) {
|
||||
field = document.querySelector('input[name*="webhook][token"]');
|
||||
}
|
||||
if (!field) {
|
||||
field = document.getElementById('scheduler-modern-webhook-token');
|
||||
}
|
||||
if (!field) {
|
||||
// Look for any text input in the webhook section
|
||||
const webhookSection = document.querySelector('.webhook_section');
|
||||
if (webhookSection) {
|
||||
const inputs = webhookSection.querySelectorAll('input[type="text"]');
|
||||
// Find the token field by checking for the placeholder
|
||||
for (let input of inputs) {
|
||||
if (input.placeholder && input.placeholder.includes('Generate')) {
|
||||
field = input;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field) {
|
||||
field.value = token;
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
field.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
// Flash the field to show it was updated
|
||||
field.style.backgroundColor = '#d4edda';
|
||||
setTimeout(function() {
|
||||
field.style.backgroundColor = '';
|
||||
}, 500);
|
||||
// Also try to trigger Grav's form change detection
|
||||
if (window.jQuery) {
|
||||
jQuery(field).trigger('change');
|
||||
}
|
||||
} else {
|
||||
// Log more debugging info
|
||||
console.error('Token field not found. Looking for input fields...');
|
||||
console.log('All inputs:', document.querySelectorAll('input[type="text"]'));
|
||||
alert('Could not find the token field. Please ensure you are in the Advanced Features tab and the Webhook Configuration section is visible.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error generating token:', e);
|
||||
alert('Error generating token: ' + e.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
markdown: false
|
||||
|
||||
modern.webhook.path:
|
||||
type: text
|
||||
label: Webhook Path
|
||||
help: URL path for webhook endpoint
|
||||
default: '/scheduler/webhook'
|
||||
placeholder: '/scheduler/webhook'
|
||||
|
||||
health_section:
|
||||
type: section
|
||||
title: Health Check Configuration
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
modern.health.enabled:
|
||||
type: toggle
|
||||
label: Enable Health Check
|
||||
help: Provide health status endpoint for monitoring
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
modern.health.path:
|
||||
type: text
|
||||
label: Health Check Path
|
||||
help: URL path for health check endpoint
|
||||
default: '/scheduler/health'
|
||||
placeholder: '/scheduler/health'
|
||||
|
||||
webhook_usage:
|
||||
type: section
|
||||
title: Usage Examples
|
||||
underline: true
|
||||
|
||||
fields:
|
||||
webhook_examples:
|
||||
type: display
|
||||
label:
|
||||
content: |
|
||||
<script src="{{ url('plugin://admin/themes/grav/js/clipboard-helper.js') }}"></script>
|
||||
<div class="webhook-examples">
|
||||
<script>
|
||||
// Initialize webhook commands when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof GravClipboard !== 'undefined') {
|
||||
GravClipboard.initWebhookCommands();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h4>How to use webhooks:</h4>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Trigger all due jobs (respects schedule):</label>
|
||||
<div class="form-input-wrapper form-input-addon-wrapper">
|
||||
<textarea id="webhook-all-cmd" readonly rows="2" style="font-family: monospace; background: #f5f5f5; resize: none;">Loading...</textarea>
|
||||
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Force-run specific job (ignores schedule):</label>
|
||||
<div class="form-input-wrapper form-input-addon-wrapper">
|
||||
<textarea id="webhook-job-cmd" readonly rows="2" style="font-family: monospace; background: #f5f5f5; resize: none;">Loading...</textarea>
|
||||
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Check health status:</label>
|
||||
<div class="form-input-wrapper form-input-addon-wrapper">
|
||||
<input type="text" id="webhook-health-cmd" readonly value="Loading..." style="font-family: monospace; background: #f5f5f5;">
|
||||
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<p><strong>GitHub Actions example:</strong></p>
|
||||
<pre>- name: Trigger Scheduler
|
||||
run: |
|
||||
curl -X POST ${{ secrets.SITE_URL }}/scheduler/webhook \
|
||||
-H "Authorization: Bearer ${{ secrets.WEBHOOK_TOKEN }}"</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
markdown: false
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -610,6 +610,15 @@ form:
|
||||
hash: All files timestamps
|
||||
none: No timestamp checking
|
||||
|
||||
cache.check.interval:
|
||||
type: number
|
||||
size: x-small
|
||||
label: Cache Check Interval
|
||||
help: Seconds to reuse the previously computed filesystem hash before checking again. Zero keeps existing per-request checks.
|
||||
validate:
|
||||
type: int
|
||||
min: 0
|
||||
|
||||
cache.driver:
|
||||
type: select
|
||||
size: small
|
||||
@@ -631,6 +640,19 @@ form:
|
||||
help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
|
||||
placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
|
||||
|
||||
cache.purge_max_age_days:
|
||||
type: text
|
||||
size: x-small
|
||||
append: GRAV.NICETIME.DAY_PLURAL
|
||||
label: PLUGIN_ADMIN.CACHE_PURGE_AGE
|
||||
help: PLUGIN_ADMIN.CACHE_PURGE_AGE_HELP
|
||||
validate:
|
||||
type: number
|
||||
min: 1
|
||||
max: 365
|
||||
step: 1
|
||||
default: 30
|
||||
|
||||
cache.purge_at:
|
||||
type: cron
|
||||
label: PLUGIN_ADMIN.CACHE_PURGE_JOB
|
||||
@@ -762,8 +784,8 @@ form:
|
||||
flex.cache.index.enabled:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.FLEX_INDEX_CACHE_ENABLED
|
||||
highlight: 1
|
||||
default: 1
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
@@ -780,8 +802,8 @@ form:
|
||||
flex.cache.object.enabled:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.FLEX_OBJECT_CACHE_ENABLED
|
||||
highlight: 1
|
||||
default: 1
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
@@ -1211,6 +1233,16 @@ form:
|
||||
title: PLUGIN_ADMIN.MEDIA
|
||||
underline: true
|
||||
|
||||
images.adapter:
|
||||
type: select
|
||||
size: small
|
||||
label: PLUGIN_ADMIN.IMAGE_ADAPTER
|
||||
help: PLUGIN_ADMIN.IMAGE_ADAPTER_HELP
|
||||
highlight: gd
|
||||
options:
|
||||
gd: GD (PHP built-in)
|
||||
imagick: Imagick
|
||||
|
||||
images.default_image_quality:
|
||||
type: range
|
||||
append: '%'
|
||||
@@ -1548,6 +1580,43 @@ form:
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
updates_section:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.UPDATES_SECTION
|
||||
|
||||
updates.safe_upgrade:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.SAFE_UPGRADE
|
||||
help: PLUGIN_ADMIN.SAFE_UPGRADE_HELP
|
||||
highlight: 1
|
||||
default: true
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
0: PLUGIN_ADMIN.NO
|
||||
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
|
||||
|
||||
updates.recovery_mode:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.RECOVERY_MODE
|
||||
help: PLUGIN_ADMIN.RECOVERY_MODE_HELP
|
||||
highlight: 1
|
||||
default: true
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
0: PLUGIN_ADMIN.NO
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
http_section:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.HTTP_SECTION
|
||||
@@ -1733,8 +1802,8 @@ form:
|
||||
http_x_forwarded.host:
|
||||
type: toggle
|
||||
label: HTTP_X_FORWARDED_HOST Enabled
|
||||
highlight: 0
|
||||
default: 0
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
0: PLUGIN_ADMIN.NO
|
||||
@@ -1767,8 +1836,8 @@ form:
|
||||
strict_mode.blueprint_compat:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.STRICT_BLUEPRINT_COMPAT
|
||||
highlight: 0
|
||||
default: 0
|
||||
highlight: 1
|
||||
default: 1
|
||||
help: PLUGIN_ADMIN.STRICT_BLUEPRINT_COMPAT_HELP
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
@@ -1788,7 +1857,7 @@ form:
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
strict_mode.twig_compat:
|
||||
strict_mode.twig2_compat:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
|
||||
highlight: 0
|
||||
@@ -1800,6 +1869,18 @@ form:
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
strict_mode.twig3_compat:
|
||||
type: toggle
|
||||
label: Twig 3 Compatibility
|
||||
highlight: 0
|
||||
default: 0
|
||||
help: Enable automatic rewrites for legacy Twig 1/2 syntax that breaks on Twig 3 (e.g. `for ... if ...` guards)
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
0: PLUGIN_ADMIN.NO
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
|
||||
accounts:
|
||||
type: tab
|
||||
@@ -1862,6 +1943,3 @@ form:
|
||||
#
|
||||
# pages.type:
|
||||
# type: hidden
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ config:
|
||||
# Edit view
|
||||
edit:
|
||||
title:
|
||||
template: "{{ form.value('fullname') ?? form.value('username') }} <{{ form.value('email') }}>"
|
||||
template: "{{ form.value('fullname') ?? form.value('username') }}"
|
||||
|
||||
# Configure view
|
||||
configure:
|
||||
|
||||
@@ -10,6 +10,7 @@ profiles:
|
||||
root: '/'
|
||||
schedule: false
|
||||
schedule_at: '0 3 * * *'
|
||||
schedule_environment: ''
|
||||
exclude_paths: "/backup\r\n/cache\r\n/images\r\n/logs\r\n/tmp"
|
||||
exclude_files: ".DS_Store\r\n.git\r\n.svn\r\n.hg\r\n.idea\r\n.vscode\r\nnode_modules"
|
||||
|
||||
|
||||
68
system/config/scheduler.yaml
Normal file
68
system/config/scheduler.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
# Grav Scheduler Configuration
|
||||
|
||||
# Default scheduler settings (backward compatible)
|
||||
defaults:
|
||||
output: true
|
||||
output_type: file
|
||||
email: null
|
||||
|
||||
# Status of individual jobs (enabled/disabled)
|
||||
status: {}
|
||||
|
||||
# Custom scheduled jobs
|
||||
custom_jobs: {}
|
||||
|
||||
# Modern scheduler features (disabled by default for backward compatibility)
|
||||
modern:
|
||||
# Enable modern scheduler features
|
||||
enabled: false
|
||||
|
||||
# Number of concurrent workers (1 = sequential execution like legacy)
|
||||
workers: 1
|
||||
|
||||
# Job retry configuration
|
||||
retry:
|
||||
enabled: true
|
||||
max_attempts: 3
|
||||
backoff: exponential # 'linear' or 'exponential'
|
||||
|
||||
# Job queue configuration
|
||||
queue:
|
||||
path: user-data://scheduler/queue
|
||||
max_size: 1000
|
||||
|
||||
# Webhook trigger configuration
|
||||
webhook:
|
||||
enabled: false
|
||||
token: null # Set a secure token to enable webhook triggers
|
||||
path: /scheduler/webhook
|
||||
|
||||
# Health check endpoint
|
||||
health:
|
||||
enabled: true
|
||||
path: /scheduler/health
|
||||
|
||||
# Job execution history
|
||||
history:
|
||||
enabled: true
|
||||
retention_days: 30
|
||||
path: user-data://scheduler/history
|
||||
|
||||
# Performance settings
|
||||
performance:
|
||||
job_timeout: 300 # Default timeout in seconds
|
||||
lock_timeout: 10 # Lock acquisition timeout in seconds
|
||||
|
||||
# Monitoring and alerts
|
||||
monitoring:
|
||||
enabled: false
|
||||
alert_on_failure: true
|
||||
alert_email: null
|
||||
webhook_url: null
|
||||
|
||||
# Trigger detection methods
|
||||
triggers:
|
||||
check_cron: true
|
||||
check_systemd: true
|
||||
check_webhook: true
|
||||
check_external: true
|
||||
@@ -1,47 +1,50 @@
|
||||
xss_whitelist: [admin.super] # Whitelist of user access that should 'skip' XSS checking
|
||||
xss_whitelist:
|
||||
- admin.super
|
||||
xss_enabled:
|
||||
on_events: true
|
||||
invalid_protocols: true
|
||||
moz_binding: true
|
||||
html_inline_styles: true
|
||||
dangerous_tags: true
|
||||
on_events: true
|
||||
invalid_protocols: true
|
||||
moz_binding: true
|
||||
html_inline_styles: true
|
||||
dangerous_tags: true
|
||||
xss_invalid_protocols:
|
||||
- javascript
|
||||
- livescript
|
||||
- vbscript
|
||||
- mocha
|
||||
- feed
|
||||
- data
|
||||
- javascript
|
||||
- livescript
|
||||
- vbscript
|
||||
- mocha
|
||||
- feed
|
||||
- data
|
||||
xss_dangerous_tags:
|
||||
- applet
|
||||
- meta
|
||||
- xml
|
||||
- blink
|
||||
- link
|
||||
- style
|
||||
- script
|
||||
- embed
|
||||
- object
|
||||
- iframe
|
||||
- frame
|
||||
- frameset
|
||||
- ilayer
|
||||
- layer
|
||||
- bgsound
|
||||
- title
|
||||
- base
|
||||
- applet
|
||||
- meta
|
||||
- xml
|
||||
- blink
|
||||
- link
|
||||
- style
|
||||
- script
|
||||
- embed
|
||||
- object
|
||||
- iframe
|
||||
- frame
|
||||
- frameset
|
||||
- ilayer
|
||||
- layer
|
||||
- bgsound
|
||||
- title
|
||||
- base
|
||||
- isindex
|
||||
uploads_dangerous_extensions:
|
||||
- php
|
||||
- php2
|
||||
- php3
|
||||
- php4
|
||||
- php5
|
||||
- phar
|
||||
- phtml
|
||||
- html
|
||||
- htm
|
||||
- shtml
|
||||
- shtm
|
||||
- js
|
||||
- exe
|
||||
- php
|
||||
- php2
|
||||
- php3
|
||||
- php4
|
||||
- php5
|
||||
- phar
|
||||
- phtml
|
||||
- html
|
||||
- htm
|
||||
- shtml
|
||||
- shtm
|
||||
- js
|
||||
- exe
|
||||
sanitize_svg: true
|
||||
salt: SbmgUJQ62MqNc0
|
||||
|
||||
@@ -93,6 +93,7 @@ cache:
|
||||
enabled: true # Set to true to enable caching
|
||||
check:
|
||||
method: file # Method to check for updates in pages: file|folder|hash|none
|
||||
interval: 0 # Seconds to reuse previous filesystem hash before rechecking (0 = every request)
|
||||
driver: auto # One of: auto|file|apcu|memcached|redis
|
||||
prefix: 'g' # Cache prefix string (prevents cache conflicts)
|
||||
purge_at: '0 4 * * *' # How often to purge old file cache (using new scheduler)
|
||||
@@ -101,6 +102,7 @@ cache:
|
||||
clear_images_by_default: false # By default grav does not include processed images in cache clear, this can be enabled
|
||||
cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcached, etc.)
|
||||
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
|
||||
purge_max_age_days: 30 # Maximum age of cache items in days before they are purged
|
||||
gzip: false # GZip compress the page output
|
||||
allow_webserver_gzip: false # If true, `content-encoding: identity` but connection isn't closed before `onShutDown()` event
|
||||
redis:
|
||||
@@ -155,6 +157,7 @@ debugger:
|
||||
close_connection: true # Close the connection before calling onShutdown(). false for debugging
|
||||
|
||||
images:
|
||||
adapter: gd # Image adapter to use: gd | imagick
|
||||
default_image_quality: 85 # Default image quality to use when resampling images (85%)
|
||||
cache_all: false # Cache all image by default
|
||||
cache_perms: '0755' # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775'
|
||||
@@ -200,6 +203,11 @@ gpm:
|
||||
releases: stable # Set to either 'stable' or 'testing'
|
||||
official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
|
||||
|
||||
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)
|
||||
recovery_mode: true # Enable recovery mode when fatal errors occur during upgrades
|
||||
|
||||
http:
|
||||
method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
|
||||
enable_proxy: true # Enable proxy server configuration
|
||||
@@ -228,5 +236,6 @@ flex:
|
||||
|
||||
strict_mode:
|
||||
yaml_compat: false # Set to true to enable YAML backwards compatibility
|
||||
twig_compat: false # Set to true to enable deprecated Twig settings (autoescape: false)
|
||||
twig2_compat: false # Set to true to enable deprecated Twig settings (autoescape: false)
|
||||
twig3_compat: true # Set to true to enable automatic fixes for Twig 3 syntax changes
|
||||
blueprint_compat: false # Set to true to enable backward compatible strict support for blueprints
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
/**
|
||||
* @package Grav\Core
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
// Some standard defines
|
||||
define('GRAV', true);
|
||||
define('GRAV_VERSION', '1.8.0-beta.2');
|
||||
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
|
||||
define('GRAV_VERSION', '1.8.0-beta.28');
|
||||
define('GRAV_SCHEMA', '1.8.0_2025-09-21_0');
|
||||
define('GRAV_TESTING', true);
|
||||
|
||||
// PHP minimum requirement
|
||||
if (!defined('GRAV_PHP_MIN')) {
|
||||
define('GRAV_PHP_MIN', '8.2.0');
|
||||
define('GRAV_PHP_MIN', '8.3.0');
|
||||
}
|
||||
|
||||
// Directory separator
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* @package Grav\Core
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,43 @@ if (!defined('GRAV_ROOT')) {
|
||||
die();
|
||||
}
|
||||
|
||||
// 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:
|
||||
// 1. Using a different class name for the NEW installer
|
||||
// 2. Or, accepting that the OLD Install class will run but ensuring it can still upgrade properly
|
||||
|
||||
// For now, use the OLD Install class but set its location to this extracted package
|
||||
// so it processes files from here
|
||||
$install = Grav\Installer\Install::instance();
|
||||
|
||||
// Use reflection to update the location property to point to this package
|
||||
$reflection = new \ReflectionClass($install);
|
||||
if ($reflection->hasProperty('location')) {
|
||||
$locationProp = $reflection->getProperty('location');
|
||||
$locationProp->setAccessible(true);
|
||||
$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;
|
||||
|
||||
@@ -119,3 +119,10 @@ GRAV:
|
||||
ERROR2: Bad number of elements
|
||||
ERROR3: The jquery_element should be set into jqCron settings
|
||||
ERROR4: Unrecognized expression
|
||||
|
||||
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).
|
||||
|
||||
547
system/recovery.php
Normal file
547
system/recovery.php
Normal file
@@ -0,0 +1,547 @@
|
||||
<?php
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Recovery\RecoveryManager;
|
||||
use Grav\Common\Upgrade\SafeUpgradeService;
|
||||
|
||||
if (!\defined('GRAV_ROOT')) {
|
||||
\define('GRAV_ROOT', dirname(__DIR__));
|
||||
}
|
||||
|
||||
session_start([
|
||||
'name' => 'grav-recovery',
|
||||
'cookie_httponly' => true,
|
||||
'cookie_secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||
'cookie_samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
$manager = new RecoveryManager();
|
||||
$context = $manager->getContext() ?? [];
|
||||
$token = $context['token'] ?? null;
|
||||
$authenticated = $token && isset($_SESSION['grav_recovery_authenticated']) && hash_equals($_SESSION['grav_recovery_authenticated'], $token);
|
||||
$errorMessage = null;
|
||||
$notice = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
if ($action === 'authenticate') {
|
||||
$provided = trim($_POST['token'] ?? '');
|
||||
if ($token && hash_equals($token, $provided)) {
|
||||
$_SESSION['grav_recovery_authenticated'] = $token;
|
||||
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||
exit;
|
||||
}
|
||||
$errorMessage = 'Invalid recovery token.';
|
||||
} elseif ($action === 'clear-flag') {
|
||||
// Clear recovery flag - allowed without authentication
|
||||
$manager->clear();
|
||||
$_SESSION['grav_recovery_authenticated'] = null;
|
||||
$notice = 'Recovery flag cleared. <a href="' . htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') . '">Reload the page</a> to continue.';
|
||||
} elseif ($action === 'disable-recovery') {
|
||||
// Disable recovery mode in config (updates.recovery_mode) - allowed without authentication
|
||||
$configDir = GRAV_ROOT . '/user/config';
|
||||
$configFile = $configDir . '/system.yaml';
|
||||
Folder::create($configDir);
|
||||
|
||||
$config = [];
|
||||
if (is_file($configFile)) {
|
||||
$content = file_get_contents($configFile);
|
||||
if ($content !== false) {
|
||||
// Simple YAML parsing for this specific case
|
||||
$config = \Symfony\Component\Yaml\Yaml::parse($content) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($config['updates'])) {
|
||||
$config['updates'] = [];
|
||||
}
|
||||
$config['updates']['recovery_mode'] = false;
|
||||
$yaml = \Symfony\Component\Yaml\Yaml::dump($config, 4, 2);
|
||||
file_put_contents($configFile, $yaml);
|
||||
|
||||
// Also clear the recovery flag
|
||||
$manager->clear();
|
||||
$_SESSION['grav_recovery_authenticated'] = null;
|
||||
$notice = 'Recovery mode has been disabled. <a href="' . htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') . '">Reload the page</a> to continue.';
|
||||
} elseif ($authenticated) {
|
||||
$service = new SafeUpgradeService();
|
||||
try {
|
||||
if ($action === 'rollback' && !empty($_POST['manifest'])) {
|
||||
$service->rollback(trim($_POST['manifest']));
|
||||
$manager->clear();
|
||||
$_SESSION['grav_recovery_authenticated'] = null;
|
||||
$notice = 'Rollback complete. Please reload Grav.';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$errorMessage = 'Authentication required for this action.';
|
||||
}
|
||||
}
|
||||
|
||||
$quarantineFile = GRAV_ROOT . '/user/data/upgrades/quarantine.json';
|
||||
$quarantine = [];
|
||||
if (is_file($quarantineFile)) {
|
||||
$decoded = json_decode(file_get_contents($quarantineFile), true);
|
||||
if (is_array($decoded)) {
|
||||
$quarantine = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
||||
$snapshots = [];
|
||||
if (is_dir($manifestDir)) {
|
||||
$files = glob($manifestDir . '/*.json');
|
||||
if ($files) {
|
||||
foreach ($files as $file) {
|
||||
$decoded = json_decode(file_get_contents($file), true);
|
||||
if (!is_array($decoded)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $decoded['id'] ?? pathinfo($file, PATHINFO_FILENAME);
|
||||
if (!is_string($id) || $id === '' || strncmp($id, 'snapshot-', 9) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$decoded['id'] = $id;
|
||||
$decoded['file'] = basename($file);
|
||||
$decoded['created_at'] = (int)($decoded['created_at'] ?? filemtime($file) ?: 0);
|
||||
$snapshots[] = $decoded;
|
||||
}
|
||||
|
||||
if ($snapshots) {
|
||||
usort($snapshots, static function (array $a, array $b): int {
|
||||
return ($b['created_at'] ?? 0) <=> ($a['created_at'] ?? 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$latestSnapshot = $snapshots[0] ?? null;
|
||||
|
||||
// Determine base URL for assets
|
||||
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
|
||||
$baseUrl = rtrim(dirname($scriptName), '/\\');
|
||||
if ($baseUrl === '.' || $baseUrl === '') {
|
||||
$baseUrl = '';
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
?><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Grav Recovery Mode</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
min-height: 100vh;
|
||||
color: #e8e8e8;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.header img {
|
||||
width: 80px;
|
||||
height: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 8px 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: #a0a0a0;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
.alert {
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #fca5a5;
|
||||
}
|
||||
.alert-warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
color: #fcd34d;
|
||||
}
|
||||
.alert-success {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
color: #86efac;
|
||||
}
|
||||
.alert-success a { color: #4ade80; }
|
||||
.alert-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.alert-content { flex: 1; }
|
||||
.alert-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 16px 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.card h3 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 12px 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error-summary {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.error-summary .error-message {
|
||||
color: #f87171;
|
||||
word-break: break-word;
|
||||
}
|
||||
.error-summary .error-location {
|
||||
color: #94a3b8;
|
||||
margin-top: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
button, .btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e8e8e8;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.btn-danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #fca5a5;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
details {
|
||||
margin-top: 16px;
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 0;
|
||||
user-select: none;
|
||||
}
|
||||
summary:hover {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.stack-trace {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 12px;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: #94a3b8;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.info-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.info-list li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.info-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.info-list .label {
|
||||
color: #94a3b8;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-list .value {
|
||||
color: #e8e8e8;
|
||||
word-break: break-word;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
label {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.help-text {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.quarantine-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.quarantine-list li {
|
||||
padding: 12px;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.quarantine-list .plugin-name {
|
||||
font-weight: 600;
|
||||
color: #fcd34d;
|
||||
}
|
||||
.quarantine-list .plugin-time {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.snapshot-info {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.snapshot-info code {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.snapshot-info small {
|
||||
color: #64748b;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 12px; }
|
||||
.card { padding: 16px; }
|
||||
.btn-group { flex-direction: column; }
|
||||
button, .btn { width: 100%; justify-content: center; }
|
||||
.info-list li { flex-direction: column; gap: 4px; }
|
||||
.info-list .label { min-width: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="<?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8'); ?>/system/assets/grav.png" alt="Grav" onerror="this.style.display='none'">
|
||||
<h1>Recovery Mode</h1>
|
||||
<p class="subtitle">Grav has encountered an error during a recent update</p>
|
||||
</div>
|
||||
|
||||
<?php if ($notice): ?>
|
||||
<div class="alert alert-success">
|
||||
<span class="alert-icon">✓</span>
|
||||
<div class="alert-content"><?php echo $notice; ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($errorMessage): ?>
|
||||
<div class="alert alert-error">
|
||||
<span class="alert-icon">⚠</span>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">Action Failed</div>
|
||||
<?php echo htmlspecialchars($errorMessage, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<span class="alert-icon">⚠</span>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">A Fatal Error Occurred</div>
|
||||
Grav detected a fatal error after a recent upgrade and has entered recovery mode to protect your site.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Error Details</h2>
|
||||
<div class="error-summary">
|
||||
<div class="error-message"><?php echo htmlspecialchars($context['message'] ?? 'Unknown error', ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<?php if (!empty($context['file'])): ?>
|
||||
<div class="error-location">
|
||||
<?php echo htmlspecialchars($context['file'], ENT_QUOTES, 'UTF-8'); ?><?php if (!empty($context['line'])): ?>:<?php echo htmlspecialchars((string)$context['line'], ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($context['trace'])): ?>
|
||||
<details>
|
||||
<summary>View Stack Trace</summary>
|
||||
<div class="stack-trace"><?php echo htmlspecialchars($context['trace'], ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($context['plugin'])): ?>
|
||||
<details open>
|
||||
<summary>Affected Plugin</summary>
|
||||
<ul class="info-list" style="margin-top: 12px;">
|
||||
<li>
|
||||
<span class="label">Plugin</span>
|
||||
<span class="value"><strong><?php echo htmlspecialchars($context['plugin'], ENT_QUOTES, 'UTF-8'); ?></strong> (has been automatically disabled)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($quarantine): ?>
|
||||
<div class="card">
|
||||
<h2>Quarantined Plugins</h2>
|
||||
<p class="help-text" style="margin-top: 0;">These plugins have been automatically disabled due to errors:</p>
|
||||
<ul class="quarantine-list">
|
||||
<?php foreach ($quarantine as $entry): ?>
|
||||
<li>
|
||||
<span class="plugin-name"><?php echo htmlspecialchars($entry['slug'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
<span class="plugin-time">Disabled at <?php echo date('Y-m-d H:i:s', $entry['disabled_at']); ?></span>
|
||||
<?php if (!empty($entry['message'])): ?>
|
||||
<div style="margin-top: 4px; font-size: 0.85rem; color: #94a3b8;"><?php echo htmlspecialchars($entry['message'], ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<h2>What would you like to do?</h2>
|
||||
<p style="margin-top: 0; color: #94a3b8;">Choose an action to resolve this issue:</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<form method="post" style="display: contents;">
|
||||
<input type="hidden" name="action" value="clear-flag">
|
||||
<button type="submit" class="btn btn-primary">Clear Recovery & Continue</button>
|
||||
</form>
|
||||
<form method="post" style="display: contents;">
|
||||
<input type="hidden" name="action" value="disable-recovery">
|
||||
<button type="submit" class="btn btn-secondary" title="Prevents recovery mode from activating in the future">Disable Recovery Mode</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="help-text">
|
||||
<strong>Clear Recovery & Continue:</strong> Clears the recovery flag and attempts to load your site normally.<br>
|
||||
<strong>Disable Recovery Mode:</strong> Sets <code>updates.recovery_mode: false</code> in your configuration so recovery mode won't trigger again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($latestSnapshot): ?>
|
||||
<div class="card">
|
||||
<h2>Rollback to Previous Version</h2>
|
||||
<p style="margin-top: 0; color: #94a3b8;">If the error persists, you can rollback to a previous Grav version.</p>
|
||||
|
||||
<div class="snapshot-info">
|
||||
<code><?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?></code>
|
||||
<?php if (!empty($latestSnapshot['label'])): ?>
|
||||
<small><?php echo htmlspecialchars($latestSnapshot['label'], ENT_QUOTES, 'UTF-8'); ?></small>
|
||||
<?php endif; ?>
|
||||
<small>Grav <?php echo htmlspecialchars($latestSnapshot['target_version'] ?? 'unknown', ENT_QUOTES, 'UTF-8'); ?> — Created <?php echo date('Y-m-d H:i:s', (int)$latestSnapshot['created_at']); ?></small>
|
||||
</div>
|
||||
|
||||
<?php if (!$authenticated): ?>
|
||||
<p class="help-text">To rollback, enter the recovery token found in <code>user/data/recovery.flag</code></p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="authenticate">
|
||||
<label for="token">Recovery Token</label>
|
||||
<input id="token" name="token" type="text" autocomplete="one-time-code" placeholder="Enter token from recovery.flag" required>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-secondary">Authenticate for Rollback</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="rollback">
|
||||
<input type="hidden" name="manifest" value="<?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-danger">Rollback to This Snapshot</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
16
system/rector.php
Normal file
16
system/rector.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Rector\Config\RectorConfig;
|
||||
|
||||
return RectorConfig::configure()
|
||||
->withSkip([
|
||||
__DIR__ . '/vendor',
|
||||
])
|
||||
->withPaths([
|
||||
__DIR__
|
||||
])
|
||||
->withPhpSets(php82: true)
|
||||
->withPhpVersion(Rector\ValueObject\PhpVersion::PHP_84)
|
||||
->withRules([
|
||||
Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector::class,
|
||||
]);
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Core
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -18,17 +18,17 @@ $path = $_SERVER['SCRIPT_NAME'];
|
||||
if ($path !== '/index.php' && is_file($root . $path)) {
|
||||
if (!(
|
||||
// Block all direct access to files and folders beginning with a dot
|
||||
strpos($path, '/.') !== false
|
||||
str_contains((string) $path, '/.')
|
||||
// Block all direct access for these folders
|
||||
|| preg_match('`^/(\.git|cache|bin|logs|backup|webserver-configs|tests)/`ui', $path)
|
||||
|| preg_match('`^/(\.git|cache|bin|logs|backup|webserver-configs|tests)/`ui', (string) $path)
|
||||
// Block access to specific file types for these system folders
|
||||
|| preg_match('`^/(system|vendor)/(.*)\.(txt|xml|md|html|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
|
||||
|| preg_match('`^/(system|vendor)/(.*)\.(txt|xml|md|html|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', (string) $path)
|
||||
// Block access to specific file types for these user folders
|
||||
|| preg_match('`^/(user)/(.*)\.(txt|md|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
|
||||
|| preg_match('`^/(user)/(.*)\.(txt|md|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', (string) $path)
|
||||
// Block all direct access to .md files
|
||||
|| preg_match('`\.md$`ui', $path)
|
||||
|| preg_match('`\.md$`ui', (string) $path)
|
||||
// Block access to specific files in the root folder
|
||||
|| preg_match('`^/(LICENSE\.txt|composer\.lock|composer\.json|\.htaccess)$`ui', $path)
|
||||
|| preg_match('`^/(LICENSE\.txt|composer\.lock|composer\.json|\.htaccess)$`ui', (string) $path)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
79
system/src/Doctrine/Common/Cache/Cache.php
Normal file
79
system/src/Doctrine/Common/Cache/Cache.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file provides a lightweight replacement for the legacy Doctrine Cache
|
||||
* interfaces so that existing Grav extensions depending on the Doctrine
|
||||
* namespace continue to function without the abandoned package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache drivers.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface Cache
|
||||
{
|
||||
public const STATS_HITS = 'hits';
|
||||
public const STATS_MISSES = 'misses';
|
||||
public const STATS_UPTIME = 'uptime';
|
||||
public const STATS_MEMORY_USAGE = 'memory_usage';
|
||||
public const STATS_MEMORY_AVAILABLE = 'memory_available';
|
||||
/**
|
||||
* Only for backward compatibility (may be removed in next major release)
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public const STATS_MEMORY_AVAILIABLE = 'memory_available';
|
||||
|
||||
/**
|
||||
* Fetches an entry from the cache.
|
||||
*
|
||||
* @param string $id The id of the cache entry to fetch.
|
||||
*
|
||||
* @return mixed The cached data or FALSE, if no cache entry exists for the given id.
|
||||
*/
|
||||
public function fetch($id);
|
||||
|
||||
/**
|
||||
* Tests if an entry exists in the cache.
|
||||
*
|
||||
* @param string $id The cache id of the entry to check for.
|
||||
*
|
||||
* @return bool TRUE if a cache entry exists for the given cache id, FALSE otherwise.
|
||||
*/
|
||||
public function contains($id);
|
||||
|
||||
/**
|
||||
* Puts data into the cache.
|
||||
*
|
||||
* If a cache entry with the given id already exists, its data will be replaced.
|
||||
*
|
||||
* @param string $id The cache id.
|
||||
* @param mixed $data The cache entry/data.
|
||||
* @param int $lifeTime The lifetime in number of seconds for this cache entry.
|
||||
* If zero (the default), the entry never expires (although it may be deleted from the cache
|
||||
* to make place for other entries).
|
||||
*
|
||||
* @return bool TRUE if the entry was successfully stored in the cache, FALSE otherwise.
|
||||
*/
|
||||
public function save($id, $data, $lifeTime = 0);
|
||||
|
||||
/**
|
||||
* Deletes a cache entry.
|
||||
*
|
||||
* @param string $id The cache id.
|
||||
*
|
||||
* @return bool TRUE if the cache entry was successfully deleted, FALSE otherwise.
|
||||
* Deleting a non-existing entry is considered successful.
|
||||
*/
|
||||
public function delete($id);
|
||||
|
||||
/**
|
||||
* Retrieves cached information from the data store.
|
||||
*
|
||||
* @return mixed[]|null An associative array with server's statistics if available, NULL otherwise.
|
||||
*/
|
||||
public function getStats();
|
||||
}
|
||||
329
system/src/Doctrine/Common/Cache/CacheProvider.php
Normal file
329
system/src/Doctrine/Common/Cache/CacheProvider.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
use function array_combine;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Base class for cache provider implementations.
|
||||
*/
|
||||
abstract class CacheProvider implements Cache, FlushableCache, ClearableCache, MultiOperationCache
|
||||
{
|
||||
public const DOCTRINE_NAMESPACE_CACHEKEY = 'DoctrineNamespaceCacheKey[%s]';
|
||||
|
||||
/**
|
||||
* The namespace to prefix all cache ids with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $namespace = '';
|
||||
|
||||
/**
|
||||
* The namespace version.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $namespaceVersion;
|
||||
|
||||
/**
|
||||
* Sets the namespace to prefix all cache ids with.
|
||||
*
|
||||
* @param string $namespace
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setNamespace($namespace)
|
||||
{
|
||||
$this->namespace = (string) $namespace;
|
||||
$this->namespaceVersion = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the namespace that prefixes all cache ids.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fetch($id)
|
||||
{
|
||||
return $this->doFetch($this->getNamespacedId($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fetchMultiple(array $keys)
|
||||
{
|
||||
if (empty($keys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// note: the array_combine() is in place to keep an association between our $keys and the $namespacedKeys
|
||||
$namespacedKeys = array_combine($keys, array_map([$this, 'getNamespacedId'], $keys));
|
||||
$items = $this->doFetchMultiple($namespacedKeys);
|
||||
$foundItems = [];
|
||||
|
||||
// no internal array function supports this sort of mapping: needs to be iterative
|
||||
// this filters and combines keys in one pass
|
||||
foreach ($namespacedKeys as $requestedKey => $namespacedKey) {
|
||||
if (! isset($items[$namespacedKey]) && ! array_key_exists($namespacedKey, $items)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$foundItems[$requestedKey] = $items[$namespacedKey];
|
||||
}
|
||||
|
||||
return $foundItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function saveMultiple(array $keysAndValues, $lifetime = 0)
|
||||
{
|
||||
$namespacedKeysAndValues = [];
|
||||
foreach ($keysAndValues as $key => $value) {
|
||||
$namespacedKeysAndValues[$this->getNamespacedId($key)] = $value;
|
||||
}
|
||||
|
||||
return $this->doSaveMultiple($namespacedKeysAndValues, $lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function contains($id)
|
||||
{
|
||||
return $this->doContains($this->getNamespacedId($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save($id, $data, $lifeTime = 0)
|
||||
{
|
||||
return $this->doSave($this->getNamespacedId($id), $data, $lifeTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteMultiple(array $keys)
|
||||
{
|
||||
return $this->doDeleteMultiple(array_map([$this, 'getNamespacedId'], $keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
return $this->doDelete($this->getNamespacedId($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
return $this->doGetStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flushAll()
|
||||
{
|
||||
return $this->doFlush();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function deleteAll()
|
||||
{
|
||||
$namespaceCacheKey = $this->getNamespaceCacheKey();
|
||||
$namespaceVersion = $this->getNamespaceVersion() + 1;
|
||||
|
||||
if ($this->doSave($namespaceCacheKey, $namespaceVersion)) {
|
||||
$this->namespaceVersion = $namespaceVersion;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the passed id with the configured namespace value.
|
||||
*
|
||||
* @param string $id The id to namespace.
|
||||
*
|
||||
* @return string The namespaced id.
|
||||
*/
|
||||
private function getNamespacedId(string $id): string
|
||||
{
|
||||
$namespaceVersion = $this->getNamespaceVersion();
|
||||
|
||||
return sprintf('%s[%s][%s]', $this->namespace, $id, $namespaceVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace cache key.
|
||||
*/
|
||||
private function getNamespaceCacheKey(): string
|
||||
{
|
||||
return sprintf(self::DOCTRINE_NAMESPACE_CACHEKEY, $this->namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace version.
|
||||
*/
|
||||
private function getNamespaceVersion(): int
|
||||
{
|
||||
if ($this->namespaceVersion !== null) {
|
||||
return $this->namespaceVersion;
|
||||
}
|
||||
|
||||
$namespaceCacheKey = $this->getNamespaceCacheKey();
|
||||
$this->namespaceVersion = (int) $this->doFetch($namespaceCacheKey) ?: 1;
|
||||
|
||||
return $this->namespaceVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of doFetchMultiple. Each driver that supports multi-get should overwrite it.
|
||||
*
|
||||
* @param string[] $keys Array of keys to retrieve from cache
|
||||
*
|
||||
* @return mixed[] Array of values retrieved for the given keys.
|
||||
*/
|
||||
protected function doFetchMultiple(array $keys)
|
||||
{
|
||||
$returnValues = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$item = $this->doFetch($key);
|
||||
if ($item === false && ! $this->doContains($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$returnValues[$key] = $item;
|
||||
}
|
||||
|
||||
return $returnValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an entry from the cache.
|
||||
*
|
||||
* @param string $id The id of the cache entry to fetch.
|
||||
*
|
||||
* @return mixed|false The cached data or FALSE, if no cache entry exists for the given id.
|
||||
*/
|
||||
abstract protected function doFetch($id);
|
||||
|
||||
/**
|
||||
* Tests if an entry exists in the cache.
|
||||
*
|
||||
* @param string $id The cache id of the entry to check for.
|
||||
*
|
||||
* @return bool TRUE if a cache entry exists for the given cache id, FALSE otherwise.
|
||||
*/
|
||||
abstract protected function doContains($id);
|
||||
|
||||
/**
|
||||
* Default implementation of doSaveMultiple. Each driver that supports multi-put should override it.
|
||||
*
|
||||
* @param mixed[] $keysAndValues Array of keys and values to save in cache
|
||||
* @param int $lifetime The lifetime. If != 0, sets a specific lifetime for these
|
||||
* cache entries (0 => infinite lifeTime).
|
||||
*
|
||||
* @return bool TRUE if the operation was successful, FALSE if it wasn't.
|
||||
*/
|
||||
protected function doSaveMultiple(array $keysAndValues, $lifetime = 0)
|
||||
{
|
||||
$success = true;
|
||||
|
||||
foreach ($keysAndValues as $key => $value) {
|
||||
if ($this->doSave($key, $value, $lifetime)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$success = false;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts data into the cache.
|
||||
*
|
||||
* @param string $id The cache id.
|
||||
* @param string $data The cache entry/data.
|
||||
* @param int $lifeTime The lifetime. If != 0, sets a specific lifetime for this
|
||||
* cache entry (0 => infinite lifeTime).
|
||||
*
|
||||
* @return bool TRUE if the entry was successfully stored in the cache, FALSE otherwise.
|
||||
*/
|
||||
abstract protected function doSave($id, $data, $lifeTime = 0);
|
||||
|
||||
/**
|
||||
* Default implementation of doDeleteMultiple. Each driver that supports multi-delete should override it.
|
||||
*
|
||||
* @param string[] $keys Array of keys to delete from cache
|
||||
*
|
||||
* @return bool TRUE if the operation was successful, FALSE if it wasn't
|
||||
*/
|
||||
protected function doDeleteMultiple(array $keys)
|
||||
{
|
||||
$success = true;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if ($this->doDelete($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$success = false;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a cache entry.
|
||||
*
|
||||
* @param string $id The cache id.
|
||||
*
|
||||
* @return bool TRUE if the cache entry was successfully deleted, FALSE otherwise.
|
||||
*/
|
||||
abstract protected function doDelete($id);
|
||||
|
||||
/**
|
||||
* Flushes all cache entries.
|
||||
*
|
||||
* @return bool TRUE if the cache entries were successfully flushed, FALSE otherwise.
|
||||
*/
|
||||
abstract protected function doFlush();
|
||||
|
||||
/**
|
||||
* Retrieves cached information from the data store.
|
||||
*
|
||||
* @return mixed[]|null An associative array with server's statistics if available, NULL otherwise.
|
||||
*/
|
||||
abstract protected function doGetStats();
|
||||
}
|
||||
25
system/src/Doctrine/Common/Cache/ClearableCache.php
Normal file
25
system/src/Doctrine/Common/Cache/ClearableCache.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache that can be flushed.
|
||||
*
|
||||
* Intended to be used for partial clearing of a cache namespace. For a more
|
||||
* global "flushing", see {@see FlushableCache}.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface ClearableCache
|
||||
{
|
||||
/**
|
||||
* Deletes all cache entries in the current cache namespace.
|
||||
*
|
||||
* @return bool TRUE if the cache entries were successfully deleted, FALSE otherwise.
|
||||
*/
|
||||
public function deleteAll();
|
||||
}
|
||||
@@ -2,92 +2,23 @@
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
use Grav\Common\Cache\SymfonyCacheProvider;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
|
||||
/**
|
||||
* Filesystem cache driver (backwards compatibility).
|
||||
*/
|
||||
class FilesystemCache extends CacheProvider
|
||||
class FilesystemCache extends SymfonyCacheProvider
|
||||
{
|
||||
public const EXTENSION = '.doctrinecache.data';
|
||||
|
||||
/** @var FilesystemAdapter */
|
||||
private $pool;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @param string $directory
|
||||
* @param string $extension
|
||||
* @param int $umask
|
||||
*/
|
||||
public function __construct($directory, $extension = self::EXTENSION, $umask = 0002)
|
||||
{
|
||||
user_error(__CLASS__ . ' is deprecated since Grav 1.8, use Symfony cache instead', E_USER_DEPRECATED);
|
||||
|
||||
$this->pool = new FilesystemAdapter('', 0, $directory);
|
||||
parent::__construct(new FilesystemAdapter('', 0, $directory));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFetch($id)
|
||||
{
|
||||
$item = $this->pool->getItem(rawurlencode($id));
|
||||
|
||||
return $item->isHit() ? $item->get() : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doContains($id)
|
||||
{
|
||||
return $this->pool->hasItem(rawurlencode($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doSave($id, $data, $lifeTime = 0)
|
||||
{
|
||||
$item = $this->pool->getItem(rawurlencode($id));
|
||||
|
||||
if (0 < $lifeTime) {
|
||||
$item->expiresAfter($lifeTime);
|
||||
}
|
||||
|
||||
return $this->pool->save($item->set($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doDelete($id)
|
||||
{
|
||||
return $this->pool->deleteItem(rawurlencode($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doFlush()
|
||||
{
|
||||
return $this->pool->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function doGetStats()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
22
system/src/Doctrine/Common/Cache/FlushableCache.php
Normal file
22
system/src/Doctrine/Common/Cache/FlushableCache.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache that can be flushed.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface FlushableCache
|
||||
{
|
||||
/**
|
||||
* Flushes all cache entries, globally.
|
||||
*
|
||||
* @return bool TRUE if the cache entries were successfully flushed, FALSE otherwise.
|
||||
*/
|
||||
public function flushAll();
|
||||
}
|
||||
26
system/src/Doctrine/Common/Cache/MultiDeleteCache.php
Normal file
26
system/src/Doctrine/Common/Cache/MultiDeleteCache.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache drivers that allows to delete many items at once.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface MultiDeleteCache
|
||||
{
|
||||
/**
|
||||
* Deletes several cache entries.
|
||||
*
|
||||
* @param string[] $keys Array of keys to delete from cache
|
||||
*
|
||||
* @return bool TRUE if the operation was successful, FALSE if it wasn't.
|
||||
*/
|
||||
public function deleteMultiple(array $keys);
|
||||
}
|
||||
27
system/src/Doctrine/Common/Cache/MultiGetCache.php
Normal file
27
system/src/Doctrine/Common/Cache/MultiGetCache.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache drivers that allows to get many items at once.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface MultiGetCache
|
||||
{
|
||||
/**
|
||||
* Returns an associative array of values for keys is found in cache.
|
||||
*
|
||||
* @param string[] $keys Array of keys to retrieve from cache
|
||||
*
|
||||
* @return mixed[] Array of retrieved values, indexed by the specified keys.
|
||||
* Values that couldn't be retrieved are not contained in this array.
|
||||
*/
|
||||
public function fetchMultiple(array $keys);
|
||||
}
|
||||
16
system/src/Doctrine/Common/Cache/MultiOperationCache.php
Normal file
16
system/src/Doctrine/Common/Cache/MultiOperationCache.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache drivers that supports multiple items manipulation.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface MultiOperationCache extends MultiGetCache, MultiDeleteCache, MultiPutCache
|
||||
{
|
||||
}
|
||||
28
system/src/Doctrine/Common/Cache/MultiPutCache.php
Normal file
28
system/src/Doctrine/Common/Cache/MultiPutCache.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight compatibility layer for the abandoned Doctrine Cache package.
|
||||
*/
|
||||
|
||||
namespace Doctrine\Common\Cache;
|
||||
|
||||
/**
|
||||
* Interface for cache drivers that allows to put many items at once.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
interface MultiPutCache
|
||||
{
|
||||
/**
|
||||
* Returns a boolean value indicating if the operation succeeded.
|
||||
*
|
||||
* @param mixed[] $keysAndValues Array of keys and values to save in cache
|
||||
* @param int $lifetime The lifetime. If != 0, sets a specific lifetime for these
|
||||
* cache entries (0 => infinite lifeTime).
|
||||
*
|
||||
* @return bool TRUE if the operation was successful, FALSE if it wasn't.
|
||||
*/
|
||||
public function saveMultiple(array $keysAndValues, $lifetime = 0);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -194,12 +194,12 @@ class Assets extends PropertyObject
|
||||
}
|
||||
|
||||
$params = array_merge([$location], $params);
|
||||
call_user_func_array([$this, 'add'], $params);
|
||||
call_user_func_array($this->add(...), $params);
|
||||
}
|
||||
} elseif (isset($this->collections[$asset])) {
|
||||
array_shift($args);
|
||||
$args = array_merge([$this->collections[$asset]], $args);
|
||||
call_user_func_array([$this, 'add'], $args);
|
||||
call_user_func_array($this->add(...), $args);
|
||||
} else {
|
||||
// Get extension
|
||||
$path = parse_url($asset, PHP_URL_PATH);
|
||||
@@ -209,11 +209,11 @@ class Assets extends PropertyObject
|
||||
if ($extension !== '') {
|
||||
$extension = strtolower($extension);
|
||||
if ($extension === 'css') {
|
||||
call_user_func_array([$this, 'addCss'], $args);
|
||||
call_user_func_array($this->addCss(...), $args);
|
||||
} elseif ($extension === 'js') {
|
||||
call_user_func_array([$this, 'addJs'], $args);
|
||||
call_user_func_array($this->addJs(...), $args);
|
||||
} elseif ($extension === 'mjs') {
|
||||
call_user_func_array([$this, 'addJsModule'], $args);
|
||||
call_user_func_array($this->addJsModule(...), $args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ class Assets extends PropertyObject
|
||||
$default = 'before';
|
||||
}
|
||||
|
||||
$options['position'] = $options['position'] ?? $default;
|
||||
$options['position'] ??= $default;
|
||||
}
|
||||
|
||||
unset($options['pipeline']);
|
||||
@@ -432,9 +432,7 @@ class Assets extends PropertyObject
|
||||
*/
|
||||
protected function sortAssets($assets)
|
||||
{
|
||||
uasort($assets, static function ($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'] ?: $a['order'] <=> $b['order'];
|
||||
});
|
||||
uasort($assets, static fn($a, $b) => $b['priority'] <=> $a['priority'] ?: $a['order'] <=> $b['order']);
|
||||
|
||||
return $assets;
|
||||
}
|
||||
@@ -464,8 +462,34 @@ class Assets extends PropertyObject
|
||||
if ($this->{$pipeline_enabled} ?? false) {
|
||||
$options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
|
||||
|
||||
$pipeline = new Pipeline($options);
|
||||
$pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
|
||||
$grouped_pipeline_assets = $this->splitPipelineAssetsByAttribute($pipeline_assets, 'loading');
|
||||
|
||||
foreach ($grouped_pipeline_assets as $pipeline_group) {
|
||||
if (empty($pipeline_group['assets'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group_attributes = array_merge($attributes, $pipeline_group['attributes']);
|
||||
|
||||
$pipeline = new Pipeline($options);
|
||||
$result = $pipeline->$render_pipeline($pipeline_group['assets'], $group, $group_attributes);
|
||||
|
||||
// Handle different return types from pipeline
|
||||
if ($result === false) {
|
||||
// No assets to render
|
||||
continue;
|
||||
} elseif (is_array($result)) {
|
||||
// Array result contains pipelined output and any failed assets
|
||||
$pipeline_output .= $result['output'];
|
||||
// Render failed assets individually (they couldn't be minified)
|
||||
foreach ($result['failed'] as $asset) {
|
||||
$pipeline_output .= $asset->render();
|
||||
}
|
||||
} else {
|
||||
// String result (no minification or CSS)
|
||||
$pipeline_output .= $result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($pipeline_assets as $asset) {
|
||||
$pipeline_output .= $asset->render();
|
||||
@@ -577,19 +601,79 @@ class Assets extends PropertyObject
|
||||
*/
|
||||
protected function getBaseType($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case $this::JS_TYPE:
|
||||
case $this::INLINE_JS_TYPE:
|
||||
$base_type = $this::JS;
|
||||
break;
|
||||
case $this::JS_MODULE_TYPE:
|
||||
case $this::INLINE_JS_MODULE_TYPE:
|
||||
$base_type = $this::JS_MODULE;
|
||||
break;
|
||||
default:
|
||||
$base_type = $this::CSS;
|
||||
}
|
||||
$base_type = match ($type) {
|
||||
$this::JS_TYPE, $this::INLINE_JS_TYPE => $this::JS,
|
||||
$this::JS_MODULE_TYPE, $this::INLINE_JS_MODULE_TYPE => $this::JS_MODULE,
|
||||
default => $this::CSS,
|
||||
};
|
||||
|
||||
return $base_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split pipeline assets into ordered groups based on the value of a given attribute.
|
||||
*
|
||||
* This preserves the original order of the assets while ensuring assets that require
|
||||
* special handling (such as different loading strategies) are rendered separately.
|
||||
*
|
||||
* @param array $assets
|
||||
* @param string $attribute
|
||||
* @return array<int, array{assets: array, attributes: array}>
|
||||
*/
|
||||
protected function splitPipelineAssetsByAttribute(array $assets, string $attribute): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentAssets = [];
|
||||
$currentValue = null;
|
||||
$hasCurrentGroup = false;
|
||||
|
||||
foreach ($assets as $key => $asset) {
|
||||
$value = null;
|
||||
|
||||
if (method_exists($asset, 'hasNestedProperty')) {
|
||||
if ($asset->hasNestedProperty($attribute)) {
|
||||
$value = $asset->getNestedProperty($attribute);
|
||||
} elseif ($asset->hasNestedProperty('attributes.' . $attribute)) {
|
||||
$value = $asset->getNestedProperty('attributes.' . $attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === null && isset($asset[$attribute])) {
|
||||
$value = $asset[$attribute];
|
||||
}
|
||||
|
||||
if ($value === '' || $value === false) {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
if (!$hasCurrentGroup) {
|
||||
$currentAssets = [$key => $asset];
|
||||
$currentValue = $value;
|
||||
$hasCurrentGroup = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === $currentValue) {
|
||||
$currentAssets[$key] = $asset;
|
||||
continue;
|
||||
}
|
||||
|
||||
$groups[] = [
|
||||
'assets' => $currentAssets,
|
||||
'attributes' => $currentValue !== null ? [$attribute => $currentValue] : []
|
||||
];
|
||||
|
||||
$currentAssets = [$key => $asset];
|
||||
$currentValue = $value;
|
||||
}
|
||||
|
||||
if ($hasCurrentGroup) {
|
||||
$groups[] = [
|
||||
'assets' => $currentAssets,
|
||||
'attributes' => $currentValue !== null ? [$attribute => $currentValue] : []
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,7 @@ abstract class BaseAsset extends PropertyObject
|
||||
|
||||
// Do some special stuff for CSS/JS (not inline)
|
||||
if (!Utils::startsWith($this->getType(), 'inline')) {
|
||||
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
|
||||
$this->base_url = rtrim((string) $uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
|
||||
$this->remote = static::isRemoteLink($asset);
|
||||
|
||||
// Move this to render?
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -192,15 +192,15 @@ class BlockAssets
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
$base = rtrim($grav['base_url'], '/') ?: '/';
|
||||
$base = rtrim((string) $grav['base_url'], '/') ?: '/';
|
||||
|
||||
if (strpos($url, $base) === 0) {
|
||||
if (str_starts_with($url, $base)) {
|
||||
if ($pipeline) {
|
||||
// Remove file timestamp if CSS pipeline has been enabled.
|
||||
$url = preg_replace('|[?#].*|', '', $url);
|
||||
}
|
||||
|
||||
return substr($url, strlen($base) - 1);
|
||||
return substr((string) $url, strlen($base) - 1);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -11,13 +11,14 @@ namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Assets\Traits\AssetUtilsTrait;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Object\PropertyObject;
|
||||
use MatthiasMullie\Minify\CSS;
|
||||
use MatthiasMullie\Minify\JS;
|
||||
use tubalmartin\CssMin\Minifier as CSSMinifier;
|
||||
use JShrink\Minifier as JSMinifier;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use function array_key_exists;
|
||||
|
||||
@@ -144,9 +145,8 @@ class Pipeline extends PropertyObject
|
||||
|
||||
// Minify if required
|
||||
if ($this->shouldMinify('css')) {
|
||||
$minifier = new CSS();
|
||||
$minifier->add($buffer);
|
||||
$buffer = $minifier->minify();
|
||||
$minifier = new CSSMinifier();
|
||||
$buffer = $minifier->run($buffer);
|
||||
}
|
||||
|
||||
// Write file
|
||||
@@ -171,7 +171,9 @@ class Pipeline extends PropertyObject
|
||||
* @param array $assets
|
||||
* @param string $group
|
||||
* @param array $attributes
|
||||
* @return bool|string URL or generated content if available, else false
|
||||
* @param int $type
|
||||
* @return array{output: string, failed: array}|string|false Returns array with output and failed assets when minifying,
|
||||
* string when not minifying, or false if no assets
|
||||
*/
|
||||
public function renderJs($assets, $group, $attributes = [], $type = self::JS_ASSET)
|
||||
{
|
||||
@@ -186,44 +188,55 @@ class Pipeline extends PropertyObject
|
||||
// Store Attributes
|
||||
$this->attributes = $attributes;
|
||||
|
||||
// Compute uid based on assets and timestamp
|
||||
$json_assets = json_encode($assets);
|
||||
$uid = md5($json_assets . $this->js_minify . $group);
|
||||
$file = $uid . '.js';
|
||||
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
|
||||
|
||||
$filepath = "{$this->assets_dir}/{$file}";
|
||||
if (file_exists($filepath)) {
|
||||
$buffer = file_get_contents($filepath) . "\n";
|
||||
} else {
|
||||
//if nothing found get out of here!
|
||||
if (empty($assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Concatenate files
|
||||
$buffer = $this->gatherLinks($assets, $type);
|
||||
|
||||
// Minify if required
|
||||
if ($this->shouldMinify('js')) {
|
||||
$minifier = new JS();
|
||||
$minifier->add($buffer);
|
||||
$buffer = $minifier->minify();
|
||||
}
|
||||
|
||||
// Write file
|
||||
if (trim($buffer) !== '') {
|
||||
file_put_contents($filepath, $buffer);
|
||||
}
|
||||
//if nothing found get out of here!
|
||||
if (empty($assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($inline_group) {
|
||||
$shouldMinify = $this->shouldMinify('js');
|
||||
$failedAssets = [];
|
||||
|
||||
// When minifying, process each file individually to isolate failures
|
||||
if ($shouldMinify) {
|
||||
$result = $this->gatherAndMinifyJs($assets, $type);
|
||||
$buffer = $result['buffer'];
|
||||
$failedAssets = $result['failed'];
|
||||
|
||||
// Compute uid based on successful assets only
|
||||
$successfulAssets = array_diff_key($assets, array_flip(array_keys($failedAssets)));
|
||||
$json_assets = json_encode($successfulAssets);
|
||||
} else {
|
||||
$buffer = $this->gatherLinks($assets, $type);
|
||||
$json_assets = json_encode($assets);
|
||||
}
|
||||
|
||||
$uid = md5($json_assets . (int)$shouldMinify . $group);
|
||||
$file = $uid . '.js';
|
||||
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
|
||||
$filepath = "{$this->assets_dir}/{$file}";
|
||||
|
||||
// Check for cached version (only if no failed assets, as cache key changes)
|
||||
if (empty($failedAssets) && file_exists($filepath)) {
|
||||
$buffer = file_get_contents($filepath) . "\n";
|
||||
} elseif (trim($buffer) !== '') {
|
||||
// Write file
|
||||
file_put_contents($filepath, $buffer);
|
||||
}
|
||||
|
||||
if (trim($buffer) === '') {
|
||||
$output = '';
|
||||
} elseif ($inline_group) {
|
||||
$output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
|
||||
} else {
|
||||
$this->asset = $relative_path;
|
||||
$output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . "></script>\n";
|
||||
}
|
||||
|
||||
// Return array with failed assets if minifying, otherwise just the output string
|
||||
if ($shouldMinify) {
|
||||
return ['output' => $output, 'failed' => $failedAssets];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
@@ -283,7 +296,7 @@ class Pipeline extends PropertyObject
|
||||
} else {
|
||||
return str_replace($matches[2], $new_url, $matches[0]);
|
||||
}
|
||||
}, $file);
|
||||
}, (string) $file);
|
||||
|
||||
return $file;
|
||||
}
|
||||
@@ -344,4 +357,75 @@ class Pipeline extends PropertyObject
|
||||
|
||||
return $minify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather JS files and minify each one individually.
|
||||
* Files that fail minification are tracked and returned separately.
|
||||
*
|
||||
* @param array $assets Array of asset objects
|
||||
* @param int $type Asset type (JS_ASSET or JS_MODULE_ASSET)
|
||||
* @return array{buffer: string, failed: array} Combined minified content and failed assets
|
||||
*/
|
||||
private function gatherAndMinifyJs(array $assets, int $type): array
|
||||
{
|
||||
$buffer = '';
|
||||
$failed = [];
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
|
||||
foreach ($assets as $key => $asset) {
|
||||
$local = true;
|
||||
$link = $asset->getAsset();
|
||||
$relative_path = $link;
|
||||
|
||||
if (static::isRemoteLink($link)) {
|
||||
$local = false;
|
||||
if (str_starts_with((string) $link, '//')) {
|
||||
$link = 'http:' . $link;
|
||||
}
|
||||
$relative_dir = dirname((string) $relative_path);
|
||||
} else {
|
||||
// Fix to remove relative dir if grav is in one
|
||||
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
|
||||
$base_url = '#' . preg_quote($this->base_url, '#') . '#';
|
||||
$relative_path = ltrim((string) preg_replace($base_url, '/', (string) $link, 1), '/');
|
||||
}
|
||||
|
||||
$relative_dir = dirname((string) $relative_path);
|
||||
$link = GRAV_ROOT . '/' . $relative_path;
|
||||
}
|
||||
|
||||
$file = $this->fetch_command instanceof \Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
|
||||
|
||||
// No file found, skip it...
|
||||
if ($file === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure proper termination
|
||||
$file = rtrim((string) $file, ' ;') . ';';
|
||||
|
||||
// Rewrite imports for JS modules
|
||||
if ($type === self::JS_MODULE_ASSET) {
|
||||
$file = $this->jsRewrite($file, $relative_dir, $local);
|
||||
}
|
||||
|
||||
// Try to minify this individual file
|
||||
try {
|
||||
$file = JSMinifier::minify($file);
|
||||
$file = rtrim($file) . PHP_EOL;
|
||||
$buffer .= $file;
|
||||
} catch (\Exception $e) {
|
||||
// Track failed asset for individual rendering
|
||||
$failed[$key] = $asset;
|
||||
|
||||
$message = "JS Minification failed for '{$asset->getAsset()}': {$e->getMessage()}";
|
||||
$debugger->addMessage($message, 'error');
|
||||
Grav::instance()['log']->error($message);
|
||||
}
|
||||
}
|
||||
|
||||
return ['buffer' => $buffer, 'failed' => $failed];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -55,7 +55,7 @@ trait AssetUtilsTrait
|
||||
return false;
|
||||
}
|
||||
|
||||
return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//'));
|
||||
return (str_starts_with($link, 'http://') || str_starts_with($link, 'https://') || str_starts_with($link, '//'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,18 +76,18 @@ trait AssetUtilsTrait
|
||||
|
||||
if (static::isRemoteLink($link)) {
|
||||
$local = false;
|
||||
if (0 === strpos($link, '//')) {
|
||||
if (str_starts_with((string) $link, '//')) {
|
||||
$link = 'http:' . $link;
|
||||
}
|
||||
$relative_dir = dirname($relative_path);
|
||||
$relative_dir = dirname((string) $relative_path);
|
||||
} else {
|
||||
// Fix to remove relative dir if grav is in one
|
||||
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
|
||||
$base_url = '#' . preg_quote($this->base_url, '#') . '#';
|
||||
$relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
|
||||
$relative_path = ltrim((string) preg_replace($base_url, '/', (string) $link, 1), '/');
|
||||
}
|
||||
|
||||
$relative_dir = dirname($relative_path);
|
||||
$relative_dir = dirname((string) $relative_path);
|
||||
$link = GRAV_ROOT . '/' . $relative_path;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ trait AssetUtilsTrait
|
||||
|
||||
// Double check last character being
|
||||
if ($type === self::JS_ASSET || $type === self::JS_MODULE_ASSET) {
|
||||
$file = rtrim($file, ' ;') . ';';
|
||||
$file = rtrim((string) $file, ' ;') . ';';
|
||||
}
|
||||
|
||||
// If this is CSS + the file is local + rewrite enabled
|
||||
@@ -113,7 +113,7 @@ trait AssetUtilsTrait
|
||||
$file = $this->jsRewrite($file, $relative_dir, $local);
|
||||
}
|
||||
|
||||
$file = rtrim($file) . PHP_EOL;
|
||||
$file = rtrim((string) $file) . PHP_EOL;
|
||||
$buffer .= $file;
|
||||
}
|
||||
|
||||
@@ -170,9 +170,9 @@ trait AssetUtilsTrait
|
||||
}
|
||||
|
||||
if (in_array($key, $no_key, true)) {
|
||||
$element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
|
||||
$element = htmlentities((string) $value, ENT_QUOTES, 'UTF-8', false);
|
||||
} else {
|
||||
$element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
|
||||
$element = $key . '="' . htmlentities((string) $value, ENT_QUOTES, 'UTF-8', false) . '"';
|
||||
}
|
||||
|
||||
$html .= ' ' . $element;
|
||||
@@ -191,7 +191,7 @@ trait AssetUtilsTrait
|
||||
{
|
||||
$querystring = '';
|
||||
|
||||
$asset = $asset ?? $this->asset;
|
||||
$asset ??= $this->asset;
|
||||
$attributes = $this->attributes;
|
||||
|
||||
if (!empty($this->query)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -113,7 +113,7 @@ trait LegacyAssetsTrait
|
||||
*/
|
||||
public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head')
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED);
|
||||
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED);
|
||||
|
||||
return $this->addJs($asset, $priority, $pipeline, 'async', $group);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ trait LegacyAssetsTrait
|
||||
*/
|
||||
public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head')
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED);
|
||||
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED);
|
||||
|
||||
return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -338,7 +338,7 @@ trait TestingAssetsTrait
|
||||
$directory,
|
||||
FilesystemIterator::SKIP_DOTS
|
||||
)), $pattern);
|
||||
$offset = strlen($ltrim);
|
||||
$offset = strlen((string) $ltrim);
|
||||
$files = [];
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Backup
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,7 @@ class Backups
|
||||
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = $grav['events'];
|
||||
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
|
||||
$dispatcher->addListener('onSchedulerInitialized', $this->onSchedulerInitialized(...));
|
||||
|
||||
$grav->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
|
||||
}
|
||||
@@ -89,8 +89,9 @@ class Backups
|
||||
$at = $profile['schedule_at'];
|
||||
$name = $inflector::hyphenize($profile['name']);
|
||||
$logs = 'logs/backup-' . $name . '.out';
|
||||
$environment = $profile['schedule_environment'] ?? null;
|
||||
/** @var Job $job */
|
||||
$job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name);
|
||||
$job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id, null, $environment], $name);
|
||||
$job->at($at);
|
||||
$job->output($logs);
|
||||
$job->backlink('/tools/backups');
|
||||
@@ -106,7 +107,7 @@ class Backups
|
||||
{
|
||||
$param_sep = Grav::instance()['config']->get('system.param_sep', ':');
|
||||
$download = urlencode(base64_encode(Utils::basename($backup)));
|
||||
$url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim(
|
||||
$url = rtrim((string) Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim(
|
||||
$base_url,
|
||||
'/'
|
||||
) . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
|
||||
@@ -158,7 +159,7 @@ class Backups
|
||||
static::$backups = [];
|
||||
|
||||
$grav = Grav::instance();
|
||||
$backups_itr = new GlobIterator(static::$backup_dir . '/*.zip', FilesystemIterator::KEY_AS_FILENAME);
|
||||
$backups_itr = new GlobIterator(static::$backup_dir . '/*.zip', FilesystemIterator::KEY_AS_FILENAME | \FilesystemIterator::SKIP_DOTS);
|
||||
$inflector = $grav['inflector'];
|
||||
$long_date_format = DATE_RFC2822;
|
||||
|
||||
@@ -192,12 +193,19 @@ class Backups
|
||||
*
|
||||
* @param int $id
|
||||
* @param callable|null $status
|
||||
* @param string|null $environment Optional environment to load config from
|
||||
* @return string|null
|
||||
*/
|
||||
public static function backup($id = 0, ?callable $status = null)
|
||||
public static function backup($id = 0, ?callable $status = null, ?string $environment = null)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
// If environment is specified and different from current, reload config
|
||||
if ($environment && $environment !== $grav['config']->get('setup.environment')) {
|
||||
$grav->setup($environment);
|
||||
$grav['config']->reload();
|
||||
}
|
||||
|
||||
$profiles = static::getBackupProfiles();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
@@ -210,7 +218,7 @@ class Backups
|
||||
|
||||
$name = $grav['inflector']->underscorize($backup->name);
|
||||
$date = date(static::BACKUP_DATE_FORMAT, time());
|
||||
$filename = trim($name, '_') . '--' . $date . '.zip';
|
||||
$filename = trim((string) $name, '_') . '--' . $date . '.zip';
|
||||
$destination = static::$backup_dir . DS . $filename;
|
||||
$max_execution_time = ini_set('max_execution_time', '600');
|
||||
$backup_root = $backup->root;
|
||||
@@ -225,6 +233,30 @@ class Backups
|
||||
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
|
||||
}
|
||||
|
||||
// Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal
|
||||
$realBackupRoot = realpath($backup_root);
|
||||
$realGravRoot = realpath(GRAV_ROOT);
|
||||
|
||||
if ($realBackupRoot === false || $realGravRoot === false) {
|
||||
throw new RuntimeException("Invalid backup location: {$backup_root}");
|
||||
}
|
||||
|
||||
// Check if backup root is within GRAV_ROOT
|
||||
$isWithinGravRoot = strpos($realBackupRoot, $realGravRoot) === 0;
|
||||
|
||||
// Only apply blocklist to paths outside GRAV_ROOT to prevent backing up system directories
|
||||
// This allows backups within Grav installations under /var/www while still blocking /var/log, etc.
|
||||
if (!$isWithinGravRoot) {
|
||||
$blockedPaths = ['/etc', '/root', '/home', '/var', '/usr', '/bin', '/sbin', '/tmp', '/proc', '/sys', '/dev'];
|
||||
foreach ($blockedPaths as $blocked) {
|
||||
if (strpos($realBackupRoot, $blocked) === 0) {
|
||||
throw new RuntimeException("Backup location not allowed: {$backup_root}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$backup_root = $realBackupRoot;
|
||||
|
||||
$options = [
|
||||
'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),
|
||||
'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),
|
||||
@@ -315,7 +347,10 @@ class Backups
|
||||
*/
|
||||
protected static function convertExclude($exclude)
|
||||
{
|
||||
$lines = preg_split("/[\s,]+/", $exclude);
|
||||
// Split by newlines, commas, or multiple spaces
|
||||
$lines = preg_split("/[\r\n,]+|[\s]{2,}/", $exclude);
|
||||
// Remove empty values and trim
|
||||
$lines = array_filter(array_map('trim', $lines));
|
||||
|
||||
return array_map('trim', $lines, array_fill(0, count($lines), '/'));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ class Browser
|
||||
{
|
||||
try {
|
||||
$this->useragent = parse_user_agent();
|
||||
} catch (InvalidArgumentException $e) {
|
||||
} catch (InvalidArgumentException) {
|
||||
$this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -11,21 +11,23 @@ namespace Grav\Common;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
|
||||
use Exception;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Scheduler\Scheduler;
|
||||
use Grav\Common\Cache\SymfonyCacheProvider;
|
||||
use LogicException;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ApcuAdapter;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
|
||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Throwable;
|
||||
use function dirname;
|
||||
use function extension_loaded;
|
||||
use function function_exists;
|
||||
@@ -34,7 +36,7 @@ use function is_array;
|
||||
|
||||
/**
|
||||
* The GravCache object is used throughout Grav to store and retrieve cached data.
|
||||
* It uses Symfony library (adding backward compatibility to Doctrine Cache) and supports a variety of caching mechanisms. Those include:
|
||||
* It uses Symfony cache pools (while exposing the historic Doctrine cache API for backward compatibility) and supports a variety of caching mechanisms. Those include:
|
||||
*
|
||||
* APCu
|
||||
* RedisCache
|
||||
@@ -160,7 +162,7 @@ class Cache extends Getters
|
||||
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = Grav::instance()['events'];
|
||||
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
|
||||
$dispatcher->addListener('onSchedulerInitialized', $this->onSchedulerInitialized(...));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,24 +178,75 @@ class Cache extends Getters
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the old out of date file-based caches
|
||||
* Deletes old cache files based on age
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function purgeOldCache()
|
||||
{
|
||||
// Get the max age for cache files from config (default 30 days)
|
||||
$max_age_days = $this->config->get('system.cache.purge_max_age_days', 30);
|
||||
$max_age_seconds = $max_age_days * 86400; // Convert days to seconds
|
||||
$now = time();
|
||||
$count = 0;
|
||||
|
||||
// First, clean up old orphaned cache directories (not the current one)
|
||||
$cache_dir = dirname($this->cache_dir);
|
||||
$current = Utils::basename($this->cache_dir);
|
||||
$count = 0;
|
||||
|
||||
|
||||
foreach (new DirectoryIterator($cache_dir) as $file) {
|
||||
$dir = $file->getBasename();
|
||||
if ($dir === $current || $file->isDot() || $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Folder::delete($file->getPathname());
|
||||
$count++;
|
||||
|
||||
// Check if directory is old and empty or very old (90+ days)
|
||||
$dir_age = $now - $file->getMTime();
|
||||
if ($dir_age > 7776000) { // 90 days
|
||||
Folder::delete($file->getPathname());
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now clean up old cache files within the current cache directory
|
||||
if (is_dir($this->cache_dir)) {
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($this->cache_dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$file_age = $now - $file->getMTime();
|
||||
if ($file_age > $max_age_seconds) {
|
||||
@unlink($file->getPathname());
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also clean up old files in compiled cache
|
||||
$grav = Grav::instance();
|
||||
$compiled_dir = $this->config->get('system.cache.compiled_dir', 'cache://compiled');
|
||||
$compiled_path = $grav['locator']->findResource($compiled_dir, true);
|
||||
|
||||
if ($compiled_path && is_dir($compiled_path)) {
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($compiled_path, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$file_age = $now - $file->getMTime();
|
||||
// Compiled files can be kept longer (60 days)
|
||||
if ($file_age > ($max_age_seconds * 2)) {
|
||||
@unlink($file->getPathname());
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
@@ -244,10 +297,28 @@ class Cache extends Getters
|
||||
public function getCacheAdapter(?string $namespace = null, ?int $defaultLifetime = null): AdapterInterface
|
||||
{
|
||||
$setting = $this->driver_setting ?? 'auto';
|
||||
$original_setting = $setting;
|
||||
$driver_name = 'file';
|
||||
$adapter = null;
|
||||
$compatibility = [
|
||||
'filesystem' => 'file',
|
||||
'files' => 'file',
|
||||
'doctrine' => 'file',
|
||||
'apc' => 'apcu',
|
||||
'memcache' => 'memcached',
|
||||
];
|
||||
|
||||
if (in_array($setting, ['apc', 'xcache', 'wincache', 'memcache'], true)) {
|
||||
throw new LogicException(sprintf('Cache driver for %s has been removed, use auto, file, apcu or memcached instead!', $setting));
|
||||
if (isset($compatibility[$setting])) {
|
||||
$mapped = $compatibility[$setting];
|
||||
if ($mapped !== $setting) {
|
||||
$this->logCacheFallback($original_setting, $mapped, 'legacy cache driver detected');
|
||||
}
|
||||
$setting = $mapped;
|
||||
}
|
||||
|
||||
if (in_array($setting, ['xcache', 'wincache'], true)) {
|
||||
$this->logCacheFallback($original_setting, 'file', 'unsupported cache driver removed in Grav 1.8');
|
||||
$setting = 'file';
|
||||
}
|
||||
|
||||
// CLI compatibility requires a non-volatile cache driver
|
||||
@@ -263,69 +334,123 @@ class Cache extends Getters
|
||||
$driver_name = $setting;
|
||||
}
|
||||
|
||||
$this->driver_name = $driver_name;
|
||||
$namespace = $namespace ?? $this->key;
|
||||
$defaultLifetime = $defaultLifetime ?? 0;
|
||||
$namespace ??= $this->key;
|
||||
$defaultLifetime ??= 0;
|
||||
$resolved_driver_name = $driver_name;
|
||||
|
||||
switch ($driver_name) {
|
||||
case 'apc':
|
||||
case 'apcu':
|
||||
$adapter = new ApcuAdapter($namespace, $defaultLifetime);
|
||||
if (extension_loaded('apcu')) {
|
||||
$adapter = new ApcuAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'apcu';
|
||||
} else {
|
||||
$this->logCacheFallback($driver_name, 'file', 'APCu extension not loaded');
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
if (extension_loaded('memcached')) {
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(
|
||||
$connected = $memcached->addServer(
|
||||
$this->config->get('system.cache.memcached.server', 'localhost'),
|
||||
$this->config->get('system.cache.memcached.port', 11211)
|
||||
);
|
||||
$adapter = new MemcachedAdapter($memcached, $namespace, $defaultLifetime);
|
||||
if ($connected) {
|
||||
$adapter = new MemcachedAdapter($memcached, $namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'memcached';
|
||||
} else {
|
||||
$this->logCacheFallback($driver_name, 'file', 'Memcached server configuration failed');
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
}
|
||||
} else {
|
||||
throw new LogicException('Memcached PHP extension has not been installed');
|
||||
$this->logCacheFallback($driver_name, 'file', 'Memcached extension not installed');
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'redis':
|
||||
if (extension_loaded('redis')) {
|
||||
$redis = new \Redis();
|
||||
$socket = $this->config->get('system.cache.redis.socket', false);
|
||||
$password = $this->config->get('system.cache.redis.password', false);
|
||||
$databaseId = $this->config->get('system.cache.redis.database', 0);
|
||||
try {
|
||||
$socket = $this->config->get('system.cache.redis.socket', false);
|
||||
$password = $this->config->get('system.cache.redis.password', false);
|
||||
$databaseId = $this->config->get('system.cache.redis.database', 0);
|
||||
|
||||
if ($socket) {
|
||||
$redis->connect($socket);
|
||||
} else {
|
||||
$redis->connect(
|
||||
$this->config->get('system.cache.redis.server', 'localhost'),
|
||||
$this->config->get('system.cache.redis.port', 6379)
|
||||
);
|
||||
if ($socket) {
|
||||
$redis->connect($socket);
|
||||
} else {
|
||||
$redis->connect(
|
||||
$this->config->get('system.cache.redis.server', 'localhost'),
|
||||
$this->config->get('system.cache.redis.port', 6379)
|
||||
);
|
||||
}
|
||||
|
||||
// Authenticate with password if set
|
||||
if ($password && !$redis->auth($password)) {
|
||||
throw new \RedisException('Redis authentication failed');
|
||||
}
|
||||
|
||||
// Select alternate ( !=0 ) database ID if set
|
||||
if ($databaseId && !$redis->select($databaseId)) {
|
||||
throw new \RedisException('Could not select alternate Redis database ID');
|
||||
}
|
||||
|
||||
$adapter = new RedisAdapter($redis, $namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'redis';
|
||||
} catch (Throwable $e) {
|
||||
$this->logCacheFallback($driver_name, 'file', $e->getMessage());
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
}
|
||||
|
||||
// Authenticate with password if set
|
||||
if ($password && !$redis->auth($password)) {
|
||||
throw new \RedisException('Redis authentication failed');
|
||||
}
|
||||
|
||||
// Select alternate ( !=0 ) database ID if set
|
||||
if ($databaseId && !$redis->select($databaseId)) {
|
||||
throw new \RedisException('Could not select alternate Redis database ID');
|
||||
}
|
||||
|
||||
$adapter = new RedisAdapter($redis, $namespace, $defaultLifetime);
|
||||
} else {
|
||||
throw new LogicException('Redis PHP extension has not been installed');
|
||||
$this->logCacheFallback($driver_name, 'file', 'Redis extension not installed');
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
$adapter = new ArrayAdapter($defaultLifetime, false);
|
||||
$adapter->setNamespace($namespace);
|
||||
$resolved_driver_name = 'array';
|
||||
break;
|
||||
|
||||
default:
|
||||
$adapter = new FilesystemAdapter($namespace, $defaultLifetime, $this->cache_dir);
|
||||
if (!in_array($driver_name, ['file', 'filesystem'], true)) {
|
||||
$this->logCacheFallback($driver_name, 'file', 'unknown cache driver');
|
||||
}
|
||||
$adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime);
|
||||
$resolved_driver_name = 'file';
|
||||
break;
|
||||
}
|
||||
|
||||
$this->driver_name = $resolved_driver_name;
|
||||
|
||||
return $adapter;
|
||||
}
|
||||
|
||||
protected function createFilesystemAdapter(string $namespace, int $defaultLifetime): FilesystemAdapter
|
||||
{
|
||||
return new FilesystemAdapter($namespace, $defaultLifetime, $this->cache_dir);
|
||||
}
|
||||
|
||||
protected function logCacheFallback(string $from, string $to, string $reason): void
|
||||
{
|
||||
try {
|
||||
$log = Grav::instance()['log'] ?? null;
|
||||
if ($log) {
|
||||
$log->warning(sprintf('Cache driver "%s" unavailable (%s); falling back to "%s".', $from, $reason, $to));
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Logging failed, continue silently.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically picks the cache mechanism to use. If you pick one manually it will use that
|
||||
* If there is no config option for $driver in the config, or it's set to 'auto', it will
|
||||
@@ -339,12 +464,12 @@ class Cache extends Getters
|
||||
$adapter = $this->getCacheAdapter();
|
||||
}
|
||||
|
||||
$cache = DoctrineProvider::wrap($adapter);
|
||||
if (!$cache instanceof CacheProvider) {
|
||||
throw new \RuntimeException('Internal error');
|
||||
$driver = new SymfonyCacheProvider($adapter);
|
||||
if ($adapter === $this->adapter) {
|
||||
$driver->setNamespace($this->key);
|
||||
}
|
||||
|
||||
return $cache;
|
||||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +567,10 @@ class Cache extends Getters
|
||||
public function setKey($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->driver->setNamespace($this->key);
|
||||
if ($this->driver instanceof CacheProvider) {
|
||||
$this->driver->setNamespace($this->key);
|
||||
}
|
||||
$this->simpleCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,8 +614,17 @@ class Cache extends Getters
|
||||
|
||||
// Delete entries in the doctrine cache if required
|
||||
if (in_array($remove, ['all', 'standard'])) {
|
||||
$cache = Grav::instance()['cache'];
|
||||
$cache->driver->deleteAll();
|
||||
try {
|
||||
$grav = Grav::instance();
|
||||
if ($grav->offsetExists('cache')) {
|
||||
$cache = $grav['cache'];
|
||||
if (isset($cache->driver)) {
|
||||
$cache->driver->deleteAll();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output[] = 'cache: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing cache event to add paths to clear
|
||||
@@ -513,6 +650,9 @@ class Cache extends Getters
|
||||
$anything = true;
|
||||
}
|
||||
} elseif (is_dir($file)) {
|
||||
if (basename($file) === 'grav-snapshots') {
|
||||
continue;
|
||||
}
|
||||
if (Folder::delete($file, false)) {
|
||||
$anything = true;
|
||||
}
|
||||
@@ -632,7 +772,7 @@ class Cache extends Getters
|
||||
*/
|
||||
public function isVolatileDriver($setting)
|
||||
{
|
||||
return $setting === 'apcu';
|
||||
return in_array($setting, ['apcu', 'array'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -646,8 +786,10 @@ class Cache extends Getters
|
||||
{
|
||||
/** @var Cache $cache */
|
||||
$cache = Grav::instance()['cache'];
|
||||
$deleted_folders = $cache->purgeOldCache();
|
||||
$msg = 'Purged ' . $deleted_folders . ' old cache folders...';
|
||||
$deleted_items = $cache->purgeOldCache();
|
||||
|
||||
$max_age = $cache->config->get('system.cache.purge_max_age_days', 30);
|
||||
$msg = 'Purged ' . $deleted_items . ' old cache items (files older than ' . $max_age . ' days)';
|
||||
|
||||
if ($echo) {
|
||||
echo $msg;
|
||||
|
||||
171
system/src/Grav/Common/Cache/SymfonyCacheProvider.php
Normal file
171
system/src/Grav/Common/Cache/SymfonyCacheProvider.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Symfony-backed cache provider that implements the legacy Doctrine Cache API.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Cache;
|
||||
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||
use function array_map;
|
||||
use function rawurlencode;
|
||||
|
||||
class SymfonyCacheProvider extends CacheProvider
|
||||
{
|
||||
/** @var AdapterInterface */
|
||||
private $adapter;
|
||||
|
||||
public function __construct(AdapterInterface $adapter)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the underlying Symfony cache pool for callers needing direct access.
|
||||
*/
|
||||
public function getAdapter(): AdapterInterface
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFetch($id)
|
||||
{
|
||||
try {
|
||||
$item = $this->adapter->getItem($this->encode($id));
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $item->isHit() ? $item->get() : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFetchMultiple(array $keys)
|
||||
{
|
||||
if (!$keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$encoded = array_map([$this, 'encode'], $keys);
|
||||
|
||||
try {
|
||||
$items = $this->adapter->getItems($encoded);
|
||||
} catch (InvalidArgumentException) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($items as $encodedKey => $item) {
|
||||
if ($item->isHit()) {
|
||||
$results[$encodedKey] = $item->get();
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doContains($id)
|
||||
{
|
||||
try {
|
||||
return $this->adapter->hasItem($this->encode($id));
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doSave($id, $data, $lifeTime = 0)
|
||||
{
|
||||
try {
|
||||
$item = $this->adapter->getItem($this->encode($id));
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($lifeTime > 0) {
|
||||
$item->expiresAfter($lifeTime);
|
||||
}
|
||||
|
||||
return $this->adapter->save($item->set($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doSaveMultiple(array $keysAndValues, $lifetime = 0)
|
||||
{
|
||||
if (!$keysAndValues) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$success = true;
|
||||
foreach ($keysAndValues as $key => $value) {
|
||||
if (!$this->doSave($key, $value, $lifetime)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doDelete($id)
|
||||
{
|
||||
try {
|
||||
return $this->adapter->deleteItem($this->encode($id));
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doDeleteMultiple(array $keys)
|
||||
{
|
||||
if (!$keys) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->adapter->deleteItems(array_map([$this, 'encode'], $keys));
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFlush()
|
||||
{
|
||||
return $this->adapter->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doGetStats()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private function encode(string $id): string
|
||||
{
|
||||
return rawurlencode($id);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,10 @@ use BadMethodCallException;
|
||||
use Exception;
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
use RuntimeException;
|
||||
use function filter_var;
|
||||
use function function_exists;
|
||||
use function get_class;
|
||||
use function ini_get;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
@@ -202,7 +205,7 @@ abstract class CompiledBase
|
||||
$cache = include $filename;
|
||||
if (!is_array($cache)
|
||||
|| !isset($cache['checksum'], $cache['data'], $cache['@class'])
|
||||
|| $cache['@class'] !== get_class($this)
|
||||
|| $cache['@class'] !== static::class
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -235,7 +238,7 @@ abstract class CompiledBase
|
||||
// Attempt to lock the file for writing.
|
||||
try {
|
||||
$file->lock(false);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// Another process has locked the file; we will check this in a bit.
|
||||
}
|
||||
|
||||
@@ -245,7 +248,7 @@ abstract class CompiledBase
|
||||
}
|
||||
|
||||
$cache = [
|
||||
'@class' => get_class($this),
|
||||
'@class' => static::class,
|
||||
'timestamp' => time(),
|
||||
'checksum' => $this->checksum(),
|
||||
'files' => $this->files,
|
||||
@@ -254,6 +257,9 @@ abstract class CompiledBase
|
||||
|
||||
$file->save($cache);
|
||||
$file->unlock();
|
||||
|
||||
$this->preloadOpcodeCache($file);
|
||||
|
||||
$file->free();
|
||||
|
||||
$this->modified();
|
||||
@@ -266,4 +272,40 @@ abstract class CompiledBase
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure compiled cache file is primed into OPcache when available.
|
||||
*/
|
||||
protected function preloadOpcodeCache(PhpFile $file): void
|
||||
{
|
||||
if (!function_exists('opcache_invalidate') || !$this->isOpcacheEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filename = $file->filename();
|
||||
if (!$filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Silence errors for restricted functions while keeping best effort behavior.
|
||||
@opcache_invalidate($filename, true);
|
||||
|
||||
if (function_exists('opcache_compile_file')) {
|
||||
@opcache_compile_file($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if OPcache is active for current SAPI.
|
||||
*/
|
||||
protected function isOpcacheEnabled(): bool
|
||||
{
|
||||
$enabled = filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$enabled = $enabled || filter_var(ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -149,7 +149,7 @@ class Config extends Data
|
||||
*/
|
||||
public function getLanguages()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use Grav::instance()[\'languages\'] instead', E_USER_DEPRECATED);
|
||||
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use Grav::instance()[\'languages\'] instead', E_USER_DEPRECATED);
|
||||
|
||||
return Grav::instance()['languages'];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -178,9 +178,7 @@ class ConfigFileFinder
|
||||
'filters' => [
|
||||
'pre-key' => $this->base,
|
||||
'key' => $pattern,
|
||||
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
|
||||
return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
|
||||
}
|
||||
'value' => fn(RecursiveDirectoryIterator $file) => ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]
|
||||
],
|
||||
'key' => 'SubPathname'
|
||||
];
|
||||
@@ -254,9 +252,7 @@ class ConfigFileFinder
|
||||
'filters' => [
|
||||
'pre-key' => $this->base,
|
||||
'key' => $pattern,
|
||||
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
|
||||
return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()];
|
||||
}
|
||||
'value' => fn(RecursiveDirectoryIterator $file) => ["{$path}/{$file->getSubPathname()}" => $file->getMTime()]
|
||||
],
|
||||
'key' => 'SubPathname'
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -202,7 +202,7 @@ class Setup extends Data
|
||||
$setupFile = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : (getenv('GRAV_SETUP_PATH') ?: null);
|
||||
if (null !== $setupFile) {
|
||||
// Make sure that the custom setup file exists. Terminates the script if not.
|
||||
if (!str_starts_with($setupFile, '/')) {
|
||||
if (!str_starts_with((string) $setupFile, '/')) {
|
||||
$setupFile = GRAV_WEBROOT . '/' . $setupFile;
|
||||
}
|
||||
if (!is_file($setupFile)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -142,7 +142,7 @@ class Blueprint extends BlueprintForm
|
||||
{
|
||||
foreach ($this->dynamic as $key => $data) {
|
||||
// Locate field.
|
||||
$path = explode('/', $key);
|
||||
$path = explode('/', (string) $key);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
@@ -168,7 +168,7 @@ class Blueprint extends BlueprintForm
|
||||
// Set dynamic property.
|
||||
foreach ($data as $property => $call) {
|
||||
$action = $call['action'];
|
||||
$method = 'dynamic' . ucfirst($action);
|
||||
$method = 'dynamic' . ucfirst((string) $action);
|
||||
$call['object'] = $this->object;
|
||||
|
||||
if (isset($this->handlers[$action])) {
|
||||
@@ -434,7 +434,7 @@ class Blueprint extends BlueprintForm
|
||||
$params = [];
|
||||
}
|
||||
|
||||
[$o, $f] = explode('::', $function, 2);
|
||||
[$o, $f] = explode('::', (string) $function, 2);
|
||||
|
||||
$data = null;
|
||||
if (!$f) {
|
||||
@@ -574,10 +574,9 @@ class Blueprint extends BlueprintForm
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public static function addPropertyRecursive(array &$field, $property, $value)
|
||||
public static function addPropertyRecursive(array &$field, $property, mixed $value)
|
||||
{
|
||||
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
|
||||
$field[$property] = array_merge_recursive($field[$property], $value);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -130,7 +130,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
foreach ($items as $key => $rules) {
|
||||
$type = $rules['type'] ?? '';
|
||||
$ignore = (bool) array_filter((array)($rules['validate']['ignore'] ?? [])) ?? false;
|
||||
if (!str_starts_with($type, '_') && !str_contains($key, '*') && $ignore !== true) {
|
||||
if (!str_starts_with((string) $type, '_') && !str_contains((string) $key, '*') && $ignore !== true) {
|
||||
$list[$prefix . $key] = null;
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,38 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
|
||||
$messages += Validation::validate($child, $rule);
|
||||
|
||||
if (isset($rule['validate']['match']) || isset($rule['validate']['match_exact']) || isset($rule['validate']['match_any'])) {
|
||||
$ruleKey = current(array_intersect(['match', 'match_exact', 'match_any'], array_keys($rule['validate'])));
|
||||
$otherKey = $rule['validate'][$ruleKey] ?? null;
|
||||
$otherVal = $data[$otherKey] ?? null;
|
||||
$otherLabel = $this->items[$otherKey]['label'] ?? $otherKey;
|
||||
$currentVal = $data[$key] ?? null;
|
||||
$currentLabel = $this->items[$key]['label'] ?? $key;
|
||||
|
||||
// Determine comparison type (loose, strict, substring)
|
||||
// Perform comparison:
|
||||
$isValid = false;
|
||||
if ($ruleKey === 'match') {
|
||||
$isValid = ($currentVal == $otherVal);
|
||||
} elseif ($ruleKey === 'match_exact') {
|
||||
$isValid = ($currentVal === $otherVal);
|
||||
} elseif ($ruleKey === 'match_any') {
|
||||
// If strings:
|
||||
if (is_string($currentVal) && is_string($otherVal)) {
|
||||
$isValid = (strlen($currentVal) && strlen($otherVal) && (str_contains($currentVal,
|
||||
$otherVal) || str_contains($otherVal, $currentVal)));
|
||||
}
|
||||
// If arrays:
|
||||
if (is_array($currentVal) && is_array($otherVal)) {
|
||||
$common = array_intersect($currentVal, $otherVal);
|
||||
$isValid = !empty($common);
|
||||
}
|
||||
}
|
||||
if (!$isValid) {
|
||||
$messages[$rule['name']][] = sprintf(Grav::instance()['language']->translate('PLUGIN_FORM.VALIDATION_MATCH'), $currentLabel, $otherLabel);
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (is_array($child) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$messages += $this->validateArray($child, $val, $strict);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -22,8 +22,6 @@ use function is_object;
|
||||
*/
|
||||
class Blueprints
|
||||
{
|
||||
/** @var array|string */
|
||||
protected $search;
|
||||
/** @var array */
|
||||
protected $types;
|
||||
/** @var array */
|
||||
@@ -32,9 +30,8 @@ class Blueprints
|
||||
/**
|
||||
* @param string|array $search Search path.
|
||||
*/
|
||||
public function __construct($search = 'blueprints://')
|
||||
public function __construct(protected $search = 'blueprints://')
|
||||
{
|
||||
$this->search = $search;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -103,7 +103,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
|
||||
* @return $this
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function join($name, $value, $separator = '.')
|
||||
public function join($name, mixed $value, $separator = '.')
|
||||
{
|
||||
$old = $this->get($name, null, $separator);
|
||||
if ($old !== null) {
|
||||
@@ -145,7 +145,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function joinDefaults($name, $value, $separator = '.')
|
||||
public function joinDefaults($name, mixed $value, $separator = '.')
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ interface DataInterface
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return mixed Value.
|
||||
*/
|
||||
public function value($name, $default = null, $separator = '.');
|
||||
public function value($name, mixed $default = null, $separator = '.');
|
||||
|
||||
/**
|
||||
* Merge external data.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -37,23 +37,24 @@ class Validation
|
||||
/**
|
||||
* Validate value against a blueprint field definition.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
public static function validate($value, array $field)
|
||||
public static function validate(mixed $value, array $field)
|
||||
{
|
||||
if (!isset($field['type'])) {
|
||||
$field['type'] = 'text';
|
||||
}
|
||||
|
||||
$validate = (array)($field['validate'] ?? null);
|
||||
$type = $validate['type'] ?? $field['type'];
|
||||
$validate_type = $validate['type'] ?? null;
|
||||
$required = $validate['required'] ?? false;
|
||||
$type = $validate_type ?? $field['type'];
|
||||
|
||||
$required = $required && ($validate_type !== 'ignore');
|
||||
|
||||
// If value isn't required, we will stop validation if empty value is given.
|
||||
if ($required !== true && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))
|
||||
) {
|
||||
if ($required !== true && ($value === null || $value === '' || empty($value) || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ class Validation
|
||||
|
||||
$messages = [];
|
||||
|
||||
$success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true;
|
||||
$success = method_exists(self::class, $method) ? self::$method($value, $validate, $field) : true;
|
||||
if (!$success) {
|
||||
$messages[$field['name']][] = $message;
|
||||
}
|
||||
@@ -85,7 +86,7 @@ class Validation
|
||||
foreach ($validate as $rule => $params) {
|
||||
$method = 'validate' . ucfirst(str_replace('-', '_', $rule));
|
||||
|
||||
if (method_exists(__CLASS__, $method)) {
|
||||
if (method_exists(self::class, $method)) {
|
||||
$success = self::$method($value, $params);
|
||||
|
||||
if (!$success) {
|
||||
@@ -98,11 +99,10 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
public static function checkSafety($value, array $field)
|
||||
public static function checkSafety(mixed $value, array $field)
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
@@ -115,7 +115,7 @@ class Validation
|
||||
$options = [];
|
||||
}
|
||||
|
||||
$name = ucfirst($field['label'] ?? $field['name'] ?? 'UNKNOWN');
|
||||
$name = ucfirst((string) ($field['label'] ?? $field['name'] ?? 'UNKNOWN'));
|
||||
|
||||
/** @var UserInterface $user */
|
||||
$user = Grav::instance()['user'] ?? null;
|
||||
@@ -186,11 +186,10 @@ class Validation
|
||||
/**
|
||||
* Filter value against a blueprint field definition.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @return mixed Filtered value.
|
||||
*/
|
||||
public static function filter($value, array $field)
|
||||
public static function filter(mixed $value, array $field)
|
||||
{
|
||||
$validate = (array)($field['filter'] ?? $field['validate'] ?? null);
|
||||
|
||||
@@ -211,7 +210,7 @@ class Validation
|
||||
$method = 'filterYaml';
|
||||
}
|
||||
|
||||
if (!method_exists(__CLASS__, $method)) {
|
||||
if (!method_exists(self::class, $method)) {
|
||||
$method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText';
|
||||
}
|
||||
|
||||
@@ -226,7 +225,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeText($value, array $params, array $field)
|
||||
public static function typeText(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!is_string($value) && !is_numeric($value)) {
|
||||
return false;
|
||||
@@ -239,7 +238,7 @@ class Validation
|
||||
}
|
||||
|
||||
$value = preg_replace("/\r\n|\r/um", "\n", $value);
|
||||
$len = mb_strlen($value);
|
||||
$len = mb_strlen((string) $value);
|
||||
|
||||
$min = (int)($params['min'] ?? 0);
|
||||
if ($min && $len < $min) {
|
||||
@@ -258,7 +257,7 @@ class Validation
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$multiline && preg_match('/\R/um', $value)) {
|
||||
if (!$multiline && preg_match('/\R/um', (string) $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -266,12 +265,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return string
|
||||
*/
|
||||
protected static function filterText($value, array $params, array $field)
|
||||
protected static function filterText(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!is_string($value) && !is_numeric($value)) {
|
||||
return '';
|
||||
@@ -287,12 +285,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function filterCheckbox($value, array $params, array $field)
|
||||
protected static function filterCheckbox(mixed $value, array $params, array $field)
|
||||
{
|
||||
$value = (string)$value;
|
||||
$field_value = (string)($field['value'] ?? '1');
|
||||
@@ -301,23 +298,21 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array|array[]|false|string[]
|
||||
*/
|
||||
protected static function filterCommaList($value, array $params, array $field)
|
||||
protected static function filterCommaList(mixed $value, array $params, array $field)
|
||||
{
|
||||
return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
return is_array($value) ? $value : preg_split('/\s*,\s*/', (string) $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return bool
|
||||
*/
|
||||
public static function typeCommaList($value, array $params, array $field)
|
||||
public static function typeCommaList(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['max'])) {
|
||||
$params['max'] = 2048;
|
||||
@@ -327,34 +322,31 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array|array[]|false|string[]
|
||||
*/
|
||||
protected static function filterLines($value, array $params, array $field)
|
||||
protected static function filterLines(mixed $value, array $params, array $field)
|
||||
{
|
||||
return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', (string) $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
protected static function filterLower($value, array $params)
|
||||
protected static function filterLower(mixed $value, array $params)
|
||||
{
|
||||
return mb_strtolower($value);
|
||||
return mb_strtolower((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
protected static function filterUpper($value, array $params)
|
||||
protected static function filterUpper(mixed $value, array $params)
|
||||
{
|
||||
return mb_strtoupper($value);
|
||||
return mb_strtoupper((string) $value);
|
||||
}
|
||||
|
||||
|
||||
@@ -366,7 +358,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeTextarea($value, array $params, array $field)
|
||||
public static function typeTextarea(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['multiline'])) {
|
||||
$params['multiline'] = true;
|
||||
@@ -383,7 +375,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typePassword($value, array $params, array $field)
|
||||
public static function typePassword(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['max'])) {
|
||||
$params['max'] = 256;
|
||||
@@ -400,7 +392,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeHidden($value, array $params, array $field)
|
||||
public static function typeHidden(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeText($value, $params, $field);
|
||||
}
|
||||
@@ -413,7 +405,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeCheckboxes($value, array $params, array $field)
|
||||
public static function typeCheckboxes(mixed $value, array $params, array $field)
|
||||
{
|
||||
// Set multiple: true so checkboxes can easily use min/max counts to control number of options required
|
||||
$field['multiple'] = true;
|
||||
@@ -422,12 +414,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function filterCheckboxes($value, array $params, array $field)
|
||||
protected static function filterCheckboxes(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::filterArray($value, $params, $field);
|
||||
}
|
||||
@@ -440,7 +431,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeCheckbox($value, array $params, array $field)
|
||||
public static function typeCheckbox(mixed $value, array $params, array $field)
|
||||
{
|
||||
$value = (string)$value;
|
||||
$field_value = (string)($field['value'] ?? '1');
|
||||
@@ -456,7 +447,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeRadio($value, array $params, array $field)
|
||||
public static function typeRadio(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
@@ -469,7 +460,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeToggle($value, array $params, array $field)
|
||||
public static function typeToggle(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
$value = (int)$value;
|
||||
@@ -486,18 +477,17 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeFile($value, array $params, array $field)
|
||||
public static function typeFile(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array)$value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
protected static function filterFile($value, array $params, array $field)
|
||||
protected static function filterFile(mixed $value, array $params, array $field)
|
||||
{
|
||||
return (array)$value;
|
||||
}
|
||||
@@ -510,7 +500,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeSelect($value, array $params, array $field)
|
||||
public static function typeSelect(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
@@ -523,7 +513,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeNumber($value, array $params, array $field)
|
||||
public static function typeNumber(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
return false;
|
||||
@@ -558,23 +548,21 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return float|int
|
||||
*/
|
||||
protected static function filterNumber($value, array $params, array $field)
|
||||
protected static function filterNumber(mixed $value, array $params, array $field)
|
||||
{
|
||||
return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return string
|
||||
*/
|
||||
protected static function filterDateTime($value, array $params, array $field)
|
||||
protected static function filterDateTime(mixed $value, array $params, array $field)
|
||||
{
|
||||
$format = Grav::instance()['config']->get('system.pages.dateformat.default');
|
||||
if ($format) {
|
||||
@@ -592,18 +580,17 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeRange($value, array $params, array $field)
|
||||
public static function typeRange(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeNumber($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return float|int
|
||||
*/
|
||||
protected static function filterRange($value, array $params, array $field)
|
||||
protected static function filterRange(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::filterNumber($value, $params, $field);
|
||||
}
|
||||
@@ -616,9 +603,9 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeColor($value, array $params, array $field)
|
||||
public static function typeColor(mixed $value, array $params, array $field)
|
||||
{
|
||||
return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
|
||||
return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', (string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -629,7 +616,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeEmail($value, array $params, array $field)
|
||||
public static function typeEmail(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
@@ -639,10 +626,10 @@ class Validation
|
||||
$params['max'] = 320;
|
||||
}
|
||||
|
||||
$values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
|
||||
$values = !is_array($value) ? explode(',', (string) preg_replace('/\s+/', '', (string) $value)) : $value;
|
||||
|
||||
foreach ($values as $val) {
|
||||
if (!(self::typeText($val, $params, $field) && strpos($val, '@', 1))) {
|
||||
if (!(self::typeText($val, $params, $field) && strpos((string) $val, '@', 1))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -658,7 +645,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeUrl($value, array $params, array $field)
|
||||
public static function typeUrl(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['max'])) {
|
||||
$params['max'] = 2048;
|
||||
@@ -675,7 +662,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDatetime($value, array $params, array $field)
|
||||
public static function typeDatetime(mixed $value, array $params, array $field)
|
||||
{
|
||||
if ($value instanceof DateTime) {
|
||||
return true;
|
||||
@@ -700,7 +687,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDatetimeLocal($value, array $params, array $field)
|
||||
public static function typeDatetimeLocal(mixed $value, array $params, array $field)
|
||||
{
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
@@ -713,7 +700,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDate($value, array $params, array $field)
|
||||
public static function typeDate(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'Y-m-d';
|
||||
@@ -730,7 +717,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeTime($value, array $params, array $field)
|
||||
public static function typeTime(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'H:i';
|
||||
@@ -747,7 +734,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeMonth($value, array $params, array $field)
|
||||
public static function typeMonth(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'Y-m';
|
||||
@@ -764,9 +751,9 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeWeek($value, array $params, array $field)
|
||||
public static function typeWeek(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
|
||||
if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', (string) $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -781,7 +768,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeArray($value, array $params, array $field)
|
||||
public static function typeArray(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return false;
|
||||
@@ -829,12 +816,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function filterFlatten_array($value, $params, $field)
|
||||
protected static function filterFlatten_array(mixed $value, $params, $field)
|
||||
{
|
||||
$value = static::filterArray($value, $params, $field);
|
||||
|
||||
@@ -842,12 +828,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function filterArray($value, $params, $field)
|
||||
protected static function filterArray(mixed $value, $params, $field)
|
||||
{
|
||||
$values = (array) $value;
|
||||
$options = isset($field['options']) ? array_keys($field['options']) : [];
|
||||
@@ -871,7 +856,7 @@ class Validation
|
||||
$val = implode(',', $val);
|
||||
$values[$key] = array_map('trim', explode(',', $val));
|
||||
} else {
|
||||
$values[$key] = trim($val);
|
||||
$values[$key] = trim((string) $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -895,16 +880,11 @@ class Validation
|
||||
{
|
||||
foreach ($values as $key => &$val) {
|
||||
if ($params['key_type']) {
|
||||
switch ($params['key_type']) {
|
||||
case 'int':
|
||||
$result = is_int($key);
|
||||
break;
|
||||
case 'string':
|
||||
$result = is_string($key);
|
||||
break;
|
||||
default:
|
||||
$result = false;
|
||||
}
|
||||
$result = match ($params['key_type']) {
|
||||
'int' => is_int($key),
|
||||
'string' => is_string($key),
|
||||
default => false,
|
||||
};
|
||||
if (!$result) {
|
||||
unset($values[$key]);
|
||||
}
|
||||
@@ -937,7 +917,7 @@ class Validation
|
||||
$val = (string)$val;
|
||||
break;
|
||||
case 'trim':
|
||||
$val = trim($val);
|
||||
$val = trim((string) $val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -952,12 +932,11 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return bool
|
||||
*/
|
||||
public static function typeList($value, array $params, array $field)
|
||||
public static function typeList(mixed $value, array $params, array $field)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return false;
|
||||
@@ -966,7 +945,7 @@ class Validation
|
||||
if (isset($field['fields'])) {
|
||||
foreach ($value as $key => $item) {
|
||||
foreach ($field['fields'] as $subKey => $subField) {
|
||||
$subKey = trim($subKey, '.');
|
||||
$subKey = trim((string) $subKey, '.');
|
||||
$subValue = $item[$subKey] ?? null;
|
||||
self::validate($subValue, $subField);
|
||||
}
|
||||
@@ -977,22 +956,20 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
protected static function filterList($value, array $params, array $field)
|
||||
protected static function filterList(mixed $value, array $params, array $field)
|
||||
{
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public static function filterYaml($value, $params)
|
||||
public static function filterYaml(mixed $value, $params)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
@@ -1009,18 +986,17 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeIgnore($value, array $params, array $field)
|
||||
public static function typeIgnore(mixed $value, array $params, array $field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return mixed
|
||||
*/
|
||||
public static function filterIgnore($value, array $params, array $field)
|
||||
public static function filterIgnore(mixed $value, array $params, array $field)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
@@ -1033,30 +1009,27 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeUnset($value, array $params, array $field)
|
||||
public static function typeUnset(mixed $value, array $params, array $field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $params
|
||||
* @param array $field
|
||||
* @return null
|
||||
*/
|
||||
public static function filterUnset($value, array $params, array $field)
|
||||
public static function filterUnset(mixed $value, array $params, array $field)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// HTML5 attributes (min, max and range are handled inside the types)
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param bool $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateRequired($value, $params)
|
||||
public static function validateRequired(mixed $value, $params)
|
||||
{
|
||||
if (is_scalar($value)) {
|
||||
return (bool) $params !== true || $value !== '';
|
||||
@@ -1066,105 +1039,85 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param string $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validatePattern($value, $params)
|
||||
public static function validatePattern(mixed $value, $params)
|
||||
{
|
||||
return (bool) preg_match("`^{$params}$`u", $value);
|
||||
return (bool) preg_match("`^{$params}$`u", (string) $value);
|
||||
}
|
||||
|
||||
// Internal types
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateAlpha($value, $params)
|
||||
public static function validateAlpha(mixed $value, mixed $params)
|
||||
{
|
||||
return ctype_alpha($value);
|
||||
return ctype_alpha((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateAlnum($value, $params)
|
||||
public static function validateAlnum(mixed $value, mixed $params)
|
||||
{
|
||||
return ctype_alnum($value);
|
||||
return ctype_alnum((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function typeBool($value, $params)
|
||||
public static function typeBool(mixed $value, mixed $params)
|
||||
{
|
||||
return is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateBool($value, $params)
|
||||
public static function validateBool(mixed $value, mixed $params)
|
||||
{
|
||||
return is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
protected static function filterBool($value, $params)
|
||||
protected static function filterBool(mixed $value, mixed $params)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateDigit($value, $params)
|
||||
public static function validateDigit(mixed $value, mixed $params)
|
||||
{
|
||||
return ctype_digit($value);
|
||||
return ctype_digit((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateFloat($value, $params)
|
||||
public static function validateFloat(mixed $value, mixed $params)
|
||||
{
|
||||
return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return float
|
||||
*/
|
||||
protected static function filterFloat($value, $params)
|
||||
protected static function filterFloat(mixed $value, mixed $params)
|
||||
{
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateHex($value, $params)
|
||||
public static function validateHex(mixed $value, mixed $params)
|
||||
{
|
||||
return ctype_xdigit($value);
|
||||
return ctype_xdigit((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1175,7 +1128,7 @@ class Validation
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeInt($value, array $params, array $field)
|
||||
public static function typeInt(mixed $value, array $params, array $field)
|
||||
{
|
||||
$params['step'] = max(1, (int)($params['step'] ?? 0));
|
||||
|
||||
@@ -1183,54 +1136,42 @@ class Validation
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateInt($value, $params)
|
||||
public static function validateInt(mixed $value, mixed $params)
|
||||
{
|
||||
return is_numeric($value) && (int)$value == $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return int
|
||||
*/
|
||||
protected static function filterInt($value, $params)
|
||||
protected static function filterInt(mixed $value, mixed $params)
|
||||
{
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateArray($value, $params)
|
||||
public static function validateArray(mixed $value, mixed $params)
|
||||
{
|
||||
return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return array
|
||||
*/
|
||||
public static function filterItem_List($value, $params)
|
||||
public static function filterItem_List(mixed $value, mixed $params)
|
||||
{
|
||||
return array_values(array_filter($value, static function ($v) {
|
||||
return !empty($v);
|
||||
}));
|
||||
return array_values(array_filter($value, static fn($v) => !empty($v)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param mixed $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateJson($value, $params)
|
||||
public static function validateJson(mixed $value, mixed $params)
|
||||
{
|
||||
return (bool) (@json_decode($value));
|
||||
return (bool) (@json_decode((string) $value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ class ValidationException extends RuntimeException implements JsonSerializable
|
||||
foreach ($messages as $list) {
|
||||
$list = array_unique($list);
|
||||
foreach ($list as $message) {
|
||||
$this->message .= '<br/>' . htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$this->message .= '<br/>' . htmlspecialchars((string) $message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ValidationException extends RuntimeException implements JsonSerializable
|
||||
$first = reset($this->messages);
|
||||
$message = reset($first);
|
||||
|
||||
$this->message = $escape ? htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $message;
|
||||
$this->message = $escape ? htmlspecialchars((string) $message, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -328,9 +328,7 @@ class Debugger
|
||||
return new Response(404, $headers, json_encode($response));
|
||||
}
|
||||
|
||||
$data = is_array($data) ? array_map(static function ($item) {
|
||||
return $item->toArray();
|
||||
}, $data) : $data->toArray();
|
||||
$data = is_array($data) ? array_map(static fn($item) => $item->toArray(), $data) : $data->toArray();
|
||||
|
||||
return new Response(200, $headers, json_encode($data));
|
||||
}
|
||||
@@ -619,17 +617,13 @@ class Debugger
|
||||
protected function buildProfilerTimings(array $timings): array
|
||||
{
|
||||
// Filter method calls which take almost no time.
|
||||
$timings = array_filter($timings, function ($value) {
|
||||
return $value['wt'] > 50;
|
||||
});
|
||||
$timings = array_filter($timings, fn($value) => $value['wt'] > 50);
|
||||
|
||||
uasort($timings, function (array $a, array $b) {
|
||||
return $b['wt'] <=> $a['wt'];
|
||||
});
|
||||
uasort($timings, fn(array $a, array $b) => $b['wt'] <=> $a['wt']);
|
||||
|
||||
$table = [];
|
||||
foreach ($timings as $key => $timing) {
|
||||
$parts = explode('==>', $key);
|
||||
$parts = explode('==>', (string) $key);
|
||||
$method = $this->parseProfilerCall(array_pop($parts));
|
||||
$context = $this->parseProfilerCall(array_pop($parts));
|
||||
|
||||
@@ -639,7 +633,7 @@ class Debugger
|
||||
}
|
||||
|
||||
// Do not profile library calls.
|
||||
if (strpos($context, 'Grav\\') !== 0) {
|
||||
if (!str_starts_with((string) $context, 'Grav\\')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -721,12 +715,11 @@ class Debugger
|
||||
/**
|
||||
* Dump variables into the Messages tab of the Debug Bar
|
||||
*
|
||||
* @param mixed $message
|
||||
* @param string $label
|
||||
* @param mixed|bool $isString
|
||||
* @return $this
|
||||
*/
|
||||
public function addMessage($message, $label = 'info', $isString = true)
|
||||
public function addMessage(mixed $message, $label = 'info', $isString = true)
|
||||
{
|
||||
if ($this->enabled) {
|
||||
if ($this->censored) {
|
||||
@@ -779,7 +772,7 @@ class Debugger
|
||||
public function addEvent(string $name, $event, EventDispatcherInterface $dispatcher, ?float $time = null)
|
||||
{
|
||||
if ($this->enabled && $this->clockwork) {
|
||||
$time = $time ?? microtime(true);
|
||||
$time ??= microtime(true);
|
||||
$duration = (microtime(true) - $time) * 1000;
|
||||
|
||||
$data = null;
|
||||
@@ -829,7 +822,7 @@ class Debugger
|
||||
public function setErrorHandler()
|
||||
{
|
||||
$this->errorHandler = set_error_handler(
|
||||
[$this, 'deprecatedErrorHandler']
|
||||
$this->deprecatedErrorHandler(...)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -858,7 +851,7 @@ class Debugger
|
||||
$scope = 'unknown';
|
||||
if (stripos($errstr, 'grav') !== false) {
|
||||
$scope = 'grav';
|
||||
} elseif (strpos($errfile, '/twig/') !== false) {
|
||||
} elseif (str_contains($errfile, '/twig/')) {
|
||||
$scope = 'twig';
|
||||
// TODO: remove when upgrading to Twig 2+
|
||||
if (str_contains($errstr, '#[\ReturnTypeWillChange]') || str_contains($errstr, 'Passing null to parameter')) {
|
||||
@@ -866,7 +859,7 @@ class Debugger
|
||||
}
|
||||
} elseif (stripos($errfile, '/yaml/') !== false) {
|
||||
$scope = 'yaml';
|
||||
} elseif (strpos($errfile, '/vendor/') !== false) {
|
||||
} elseif (str_contains($errfile, '/vendor/')) {
|
||||
$scope = 'vendor';
|
||||
}
|
||||
|
||||
@@ -912,7 +905,7 @@ class Debugger
|
||||
} elseif (is_scalar($arg)) {
|
||||
$arg = $arg;
|
||||
} elseif (is_object($arg)) {
|
||||
$arg = get_class($arg) . ' $object';
|
||||
$arg = $arg::class . ' $object';
|
||||
} elseif (is_array($arg)) {
|
||||
$arg = '$array';
|
||||
} else {
|
||||
@@ -931,14 +924,13 @@ class Debugger
|
||||
if ($object instanceof TemplateWrapper) {
|
||||
$reflection = new ReflectionObject($object);
|
||||
$property = $reflection->getProperty('template');
|
||||
$property->setAccessible(true);
|
||||
$object = $property->getValue($object);
|
||||
}
|
||||
|
||||
if ($object instanceof Template) {
|
||||
$file = $current['file'] ?? null;
|
||||
|
||||
if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
|
||||
if (preg_match('`(Template.php|TemplateWrapper.php)$`', (string) $file)) {
|
||||
$current = null;
|
||||
continue;
|
||||
}
|
||||
@@ -998,7 +990,7 @@ class Debugger
|
||||
if (!isset($current['file'])) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($current['file'], '/vendor/') !== false) {
|
||||
if (str_contains($current['file'], '/vendor/')) {
|
||||
$cut = $i + 1;
|
||||
continue;
|
||||
}
|
||||
@@ -1073,7 +1065,7 @@ class Debugger
|
||||
|
||||
/** @var array $deprecated */
|
||||
foreach ($this->deprecations as $deprecated) {
|
||||
list($message, $scope) = $this->getDepracatedMessage($deprecated);
|
||||
[$message, $scope] = $this->getDepracatedMessage($deprecated);
|
||||
|
||||
$collector->addMessage($message, $scope);
|
||||
}
|
||||
@@ -1140,7 +1132,7 @@ class Debugger
|
||||
protected function resolveCallable(callable $callable)
|
||||
{
|
||||
if (is_array($callable)) {
|
||||
return get_class($callable[0]) . '->' . $callable[1] . '()';
|
||||
return $callable[0]::class . '->' . $callable[1] . '()';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -54,11 +54,7 @@ class SimplePageHandler extends Handler
|
||||
$code = Misc::translateErrorCode($code);
|
||||
}
|
||||
|
||||
$vars = array(
|
||||
'stylesheet' => file_get_contents($cssFile),
|
||||
'code' => $code,
|
||||
'message' => htmlspecialchars(strip_tags(rawurldecode($message)), ENT_QUOTES, 'UTF-8'),
|
||||
);
|
||||
$vars = ['stylesheet' => file_get_contents($cssFile), 'code' => $code, 'message' => htmlspecialchars(strip_tags(rawurldecode($message)), ENT_QUOTES, 'UTF-8')];
|
||||
|
||||
$helper->setVariables($vars);
|
||||
$helper->render($templateFile);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,7 @@ trait CompiledFile
|
||||
* @param mixed $var
|
||||
* @return array
|
||||
*/
|
||||
public function content($var = null)
|
||||
public function content(mixed $var = null)
|
||||
{
|
||||
try {
|
||||
$filename = $this->filename;
|
||||
@@ -39,18 +39,43 @@ trait CompiledFile
|
||||
if ($var === null && $this->raw === null && $this->content === null) {
|
||||
$key = md5($filename);
|
||||
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
|
||||
$cacheFilename = $file->filename();
|
||||
|
||||
// Always check file modification time for cache invalidation.
|
||||
// This respects Grav's cache.check.method setting and user expectations.
|
||||
// filemtime() is cheap and ensures changes are detected.
|
||||
$modified = $this->modified();
|
||||
if (!$modified) {
|
||||
|
||||
// If file hasn't been modified and cache exists, load from compiled cache.
|
||||
// When opcache is enabled, this benefits from bytecode caching.
|
||||
if (!$modified && is_file($cacheFilename)) {
|
||||
try {
|
||||
return $this->decode($this->raw());
|
||||
} catch (Throwable $e) {
|
||||
// Include the file directly to trigger loading from opcache
|
||||
$var = (array) include $cacheFilename;
|
||||
|
||||
if (is_array($var) && isset($var['data'])) {
|
||||
$var = $var['data'];
|
||||
} else {
|
||||
$var = null;
|
||||
}
|
||||
|
||||
if (!is_array($var)) {
|
||||
$var = $this->decode($this->raw());
|
||||
}
|
||||
|
||||
return $var;
|
||||
} catch (Throwable) {
|
||||
// If the compiled file is broken, we can safely ignore the error and continue.
|
||||
}
|
||||
}
|
||||
|
||||
$class = get_class($this);
|
||||
|
||||
// Check if the source file exists before getting its size
|
||||
if (!is_file($filename)) {
|
||||
return parent::content($var);
|
||||
}
|
||||
|
||||
$size = filesize($filename);
|
||||
$cache = $file->exists() ? $file->content() : null;
|
||||
|
||||
@@ -89,11 +114,9 @@ trait CompiledFile
|
||||
|
||||
// Compile cached file into bytecode cache
|
||||
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
|
||||
$lockName = $file->filename();
|
||||
|
||||
// Silence error if function exists, but is restricted.
|
||||
@opcache_invalidate($lockName, true);
|
||||
@opcache_compile_file($lockName);
|
||||
@opcache_invalidate($cacheFilename, true);
|
||||
@opcache_compile_file($cacheFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +138,7 @@ trait CompiledFile
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function save($data = null)
|
||||
public function save(mixed $data = null)
|
||||
{
|
||||
// Make sure that the cache file is always up to date!
|
||||
$key = md5($this->filename);
|
||||
@@ -159,10 +182,10 @@ trait CompiledFile
|
||||
|
||||
// Compile cached file into bytecode cache
|
||||
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
|
||||
$lockName = $file->filename();
|
||||
$cacheFilename = $file->filename();
|
||||
// Silence error if function exists, but is restricted.
|
||||
@opcache_invalidate($lockName, true);
|
||||
@opcache_compile_file($lockName);
|
||||
@opcache_invalidate($cacheFilename, true);
|
||||
@opcache_compile_file($cacheFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -153,8 +153,8 @@ abstract class Folder
|
||||
if ($base) {
|
||||
$base = preg_replace('![\\\/]+!', '/', $base);
|
||||
$path = preg_replace('![\\\/]+!', '/', $path);
|
||||
if (strpos($path, $base) === 0) {
|
||||
$path = ltrim(substr($path, strlen($base)), '/');
|
||||
if (str_starts_with((string) $path, (string) $base)) {
|
||||
$path = ltrim(substr((string) $path, strlen((string) $base)), '/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +178,8 @@ abstract class Folder
|
||||
return '';
|
||||
}
|
||||
|
||||
$baseParts = explode('/', ltrim($base, '/'));
|
||||
$pathParts = explode('/', ltrim($path, '/'));
|
||||
$baseParts = explode('/', ltrim((string) $base, '/'));
|
||||
$pathParts = explode('/', ltrim((string) $path, '/'));
|
||||
|
||||
array_pop($baseParts);
|
||||
$lastPart = array_pop($pathParts);
|
||||
@@ -194,7 +194,7 @@ abstract class Folder
|
||||
$path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
|
||||
|
||||
return '' === $path
|
||||
|| strpos($path, '/') === 0
|
||||
|| str_starts_with($path, '/')
|
||||
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
|
||||
? "./$path" : $path;
|
||||
}
|
||||
@@ -266,7 +266,7 @@ abstract class Folder
|
||||
/** @var RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $file) {
|
||||
// Ignore hidden files.
|
||||
if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
|
||||
if (str_starts_with($file->getFilename(), '.') && $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (!$folders && $file->isDir()) {
|
||||
@@ -275,7 +275,7 @@ abstract class Folder
|
||||
if (!$files && $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
|
||||
if ($compare && $pattern && !preg_match($pattern, (string) $file->{$compare}())) {
|
||||
continue;
|
||||
}
|
||||
$fileKey = $key ? $file->{$key}() : null;
|
||||
@@ -283,14 +283,14 @@ abstract class Folder
|
||||
if ($filters) {
|
||||
if (isset($filters['key'])) {
|
||||
$pre = !empty($filters['pre-key']) ? $filters['pre-key'] : '';
|
||||
$fileKey = $pre . preg_replace($filters['key'], '', $fileKey);
|
||||
$fileKey = $pre . preg_replace($filters['key'], '', (string) $fileKey);
|
||||
}
|
||||
if (isset($filters['value'])) {
|
||||
$filter = $filters['value'];
|
||||
if (is_callable($filter)) {
|
||||
$filePath = $filter($file);
|
||||
} else {
|
||||
$filePath = preg_replace($filter, '', $filePath);
|
||||
$filePath = preg_replace($filter, '', (string) $filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +331,7 @@ abstract class Folder
|
||||
// Go through all sub-directories and copy everything.
|
||||
$files = self::all($source);
|
||||
foreach ($files as $file) {
|
||||
if ($ignore && preg_match($ignore, $file)) {
|
||||
if ($ignore && preg_match($ignore, (string) $file)) {
|
||||
continue;
|
||||
}
|
||||
$src = $source .'/'. $file;
|
||||
@@ -377,7 +377,7 @@ abstract class Folder
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos($target, $source . '/') === 0) {
|
||||
if (str_starts_with($target, $source . '/')) {
|
||||
throw new RuntimeException('Cannot move folder to itself');
|
||||
}
|
||||
|
||||
@@ -478,12 +478,22 @@ abstract class Folder
|
||||
* @return bool
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function rcopy($src, $dest)
|
||||
public static function rcopy($src, $dest, $preservePermissions = false)
|
||||
{
|
||||
|
||||
// If the src is not a directory do a simple file copy
|
||||
if (!is_dir($src)) {
|
||||
copy($src, $dest);
|
||||
if ($preservePermissions) {
|
||||
$perm = @fileperms($src);
|
||||
if ($perm !== false) {
|
||||
@chmod($dest, $perm & 0777);
|
||||
}
|
||||
$mtime = @filemtime($src);
|
||||
if ($mtime !== false) {
|
||||
@touch($dest, $mtime);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -492,14 +502,32 @@ abstract class Folder
|
||||
static::create($dest);
|
||||
}
|
||||
|
||||
if ($preservePermissions) {
|
||||
$perm = @fileperms($src);
|
||||
if ($perm !== false) {
|
||||
@chmod($dest, $perm & 0777);
|
||||
}
|
||||
}
|
||||
|
||||
// Open the source directory to read in files
|
||||
$i = new DirectoryIterator($src);
|
||||
foreach ($i as $f) {
|
||||
if ($f->isFile()) {
|
||||
copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
|
||||
$target = "{$dest}/" . $f->getFilename();
|
||||
copy($f->getRealPath(), $target);
|
||||
if ($preservePermissions) {
|
||||
$perm = @fileperms($f->getRealPath());
|
||||
if ($perm !== false) {
|
||||
@chmod($target, $perm & 0777);
|
||||
}
|
||||
$mtime = @filemtime($f->getRealPath());
|
||||
if ($mtime !== false) {
|
||||
@touch($target, $mtime);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$f->isDot() && $f->isDir()) {
|
||||
static::rcopy($f->getRealPath(), "{$dest}/{$f}");
|
||||
static::rcopy($f->getRealPath(), "{$dest}/{$f}", $preservePermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -57,17 +57,54 @@ class RecursiveDirectoryFilterIterator extends RecursiveFilterIterator
|
||||
$relative_filename = str_replace($this::$root . '/', '', $file->getPathname());
|
||||
|
||||
if ($file->isDir()) {
|
||||
// Check if the directory path is in the ignore list
|
||||
if (in_array($relative_filename, $this::$ignore_folders, true)) {
|
||||
return false;
|
||||
}
|
||||
if (!in_array($filename, $this::$ignore_files, true)) {
|
||||
// Check if any parent directory is in the ignore list
|
||||
foreach ($this::$ignore_folders as $ignore_folder) {
|
||||
$ignore_folder = trim((string) $ignore_folder, '/');
|
||||
if (str_starts_with($relative_filename, $ignore_folder . '/') || $relative_filename === $ignore_folder) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!$this->matchesPattern($filename, $this::$ignore_files)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($file->isFile() && !in_array($filename, $this::$ignore_files, true)) {
|
||||
} elseif ($file->isFile() && !$this->matchesPattern($filename, $this::$ignore_files)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filename matches any pattern in the list
|
||||
*
|
||||
* @param string $filename
|
||||
* @param array $patterns
|
||||
* @return bool
|
||||
*/
|
||||
protected function matchesPattern($filename, $patterns)
|
||||
{
|
||||
foreach ($patterns as $pattern) {
|
||||
// Check for exact match
|
||||
if ($filename === $pattern) {
|
||||
return true;
|
||||
}
|
||||
// Check for extension patterns like .pdf
|
||||
if (str_starts_with((string) $pattern, '.') && str_ends_with($filename, (string) $pattern)) {
|
||||
return true;
|
||||
}
|
||||
// Check for wildcard patterns
|
||||
if (str_contains((string) $pattern, '*')) {
|
||||
$regex = '/^' . str_replace('\\*', '.*', preg_quote((string) $pattern, '/')) . '$/';
|
||||
if (preg_match($regex, $filename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RecursiveDirectoryFilterIterator|RecursiveFilterIterator
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -64,8 +64,21 @@ class ZipArchiver extends Archiver
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if (!$zip->open($this->archive_file, ZipArchive::CREATE)) {
|
||||
throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
|
||||
$result = $zip->open($this->archive_file, ZipArchive::CREATE);
|
||||
if ($result !== true) {
|
||||
$error = 'unknown error';
|
||||
if ($result === ZipArchive::ER_NOENT) {
|
||||
$error = 'file does not exist';
|
||||
} elseif ($result === ZipArchive::ER_EXISTS) {
|
||||
$error = 'file already exists';
|
||||
} elseif ($result === ZipArchive::ER_OPEN) {
|
||||
$error = 'cannot open file';
|
||||
} elseif ($result === ZipArchive::ER_READ) {
|
||||
$error = 'read error';
|
||||
} elseif ($result === ZipArchive::ER_SEEK) {
|
||||
$error = 'seek error';
|
||||
}
|
||||
throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be created: ' . $error);
|
||||
}
|
||||
|
||||
$files = $this->getArchiveFiles($rootPath);
|
||||
@@ -77,7 +90,7 @@ class ZipArchiver extends Archiver
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filePath = $file->getPathname();
|
||||
$relativePath = ltrim(substr($filePath, strlen($rootPath)), '/');
|
||||
$relativePath = ltrim(substr((string) $filePath, strlen($rootPath)), '/');
|
||||
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($relativePath);
|
||||
@@ -112,8 +125,21 @@ class ZipArchiver extends Archiver
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if (!$zip->open($this->archive_file)) {
|
||||
throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...');
|
||||
$result = $zip->open($this->archive_file);
|
||||
if ($result !== true) {
|
||||
$error = 'unknown error';
|
||||
if ($result === ZipArchive::ER_NOENT) {
|
||||
$error = 'file does not exist';
|
||||
} elseif ($result === ZipArchive::ER_EXISTS) {
|
||||
$error = 'file already exists';
|
||||
} elseif ($result === ZipArchive::ER_OPEN) {
|
||||
$error = 'cannot open file';
|
||||
} elseif ($result === ZipArchive::ER_READ) {
|
||||
$error = 'read error';
|
||||
} elseif ($result === ZipArchive::ER_SEEK) {
|
||||
$error = 'seek error';
|
||||
}
|
||||
throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened: ' . $error);
|
||||
}
|
||||
|
||||
$status && $status([
|
||||
@@ -122,7 +148,12 @@ class ZipArchiver extends Archiver
|
||||
]);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$zip->addEmptyDir($folder);
|
||||
if ($zip->addEmptyDir($folder) === false) {
|
||||
$status && $status([
|
||||
'type' => 'message',
|
||||
'message' => 'Warning: Could not add empty directory: ' . $folder
|
||||
]);
|
||||
}
|
||||
$status && $status([
|
||||
'type' => 'progress',
|
||||
]);
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -35,7 +35,7 @@ trait FlexCollectionTrait
|
||||
'collection' => $this
|
||||
]);
|
||||
}
|
||||
if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
|
||||
if (!str_starts_with($name, 'onFlexCollection') && str_starts_with($name, 'on')) {
|
||||
$name = 'onFlexCollection' . substr($name, 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -46,7 +46,7 @@ trait FlexObjectTrait
|
||||
|
||||
if (isset($events['name'])) {
|
||||
$name = $events['name'];
|
||||
} elseif (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) {
|
||||
} elseif (!str_starts_with($name, 'onFlexObject') && str_starts_with($name, 'on')) {
|
||||
$name = 'onFlexObject' . substr($name, 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user