Compare commits

...

255 Commits

Author SHA1 Message Date
Andy Miller
1bc6e5e13a prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-22 11:17:37 +00:00
Andy Miller
f339bb83c5 update composer
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-22 11:16:08 +00:00
Andy Miller
27789991ae prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-21 12:45:03 +00:00
Andy Miller
114aebae7c more robust deferred logic + deprecated fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-21 12:25:58 +00:00
Andy Miller
370dfd6016 updated vendor libs
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-21 11:37:28 +00:00
Andy Miller
1d05e6bdc4 pages rebuild optimization
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-21 11:16:34 +00:00
Andy Miller
3acff8a9f8 update changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-20 11:18:20 +00:00
Andy Miller
ea59bdb1d4 Fix double execution of preflight checks during self-upgrade 2025-11-20 11:16:07 +00:00
Andy Miller
02330b96d9 Optimize preflight Monolog checks by skipping vendor directories 2025-11-20 11:12:03 +00:00
Andy Miller
2b1d73fd26 fix for slow tests
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-20 10:54:24 +00:00
Andy Miller
4e11ca7c8e Fix slow SafeUpgradeServiceTest by optimizing snapshot pruning 2025-11-20 10:51:45 +00:00
Andy Miller
591e2e4563 revert missing line
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-19 22:26:59 +00:00
Andy Miller
2161ffeb5e gated the debugger addEvent call
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-18 21:55:55 +00:00
Andy Miller
b856978211 reuse regex for better optimization
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-18 21:50:35 +00:00
Andy Miller
19ee2d883e lazy load page optimization
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-18 21:24:50 +00:00
Andy Miller
93089241c3 Ensure file permissions are preserved during safe-upgrade copy operations 2025-11-18 18:28:46 +00:00
Andy Miller
3b1c332932 Fix safe-upgrade snapshot creation (copy vs move) and implement pruning 2025-11-18 18:21:34 +00:00
Andy Miller
7fd614f8b6 Add Twig 3 compatibility transformations for raw, divisibleby, and none 2025-11-18 17:46:20 +00:00
Andy Miller
5567a5a1cd twig3 compatibility fixes + tests
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-18 17:25:07 +00:00
Andy Miller
334e1dcabc prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 14:42:09 +00:00
Andy Miller
cbf5ec57c6 test fixes + major/minor plugin warnings
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:33:24 +00:00
Andy Miller
9f33e247cf added configurable snapshot pruning amount
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:33:07 +00:00
Andy Miller
8c7e970603 some installer fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:33:04 +00:00
Andy Miller
360b418c97 checkout correct version
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:33:01 +00:00
Andy Miller
af0db0c2a1 preflight integration for cli
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:32:58 +00:00
Andy Miller
4c74192191 ui things
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:32:56 +00:00
Andy Miller
ee5fccd2c8 added back snapshots in Install.php
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:32:52 +00:00
Andy Miller
5bc89bf32b simplified safe-upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:32:49 +00:00
Andy Miller
0b021e2114 more simplification
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:29:12 +00:00
Andy Miller
15c1b1cc06 simplify copy/permission process + fix safe-upgrade check
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-14 11:29:07 +00:00
Andy Miller
ee1b55e929 don’t error when trying to force —safe
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 15:50:51 +00:00
Andy Miller
73d3a90c0b test fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 13:11:17 +00:00
Andy Miller
0764e37c8b major/minor upgrade warnings
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 12:51:54 +00:00
Andy Miller
bd5b2633f7 less confusing messages
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 12:07:34 +00:00
Andy Miller
6b0c0486aa new minifier libraries
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 11:58:35 +00:00
Andy Miller
07ac3d3bb9 vendor updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-09 11:47:42 +00:00
Andy Miller
72e9d57e2e fall back to safe upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-08 20:05:25 +00:00
Andy Miller
07965c6c61 revert testing repo
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-08 12:18:09 +00:00
Andy Miller
72cc8e91a2 some more fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-08 11:44:18 +00:00
Andy Miller
678eacaae5 fix some errors after upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-08 11:31:27 +00:00
Andy Miller
cb7a3ccfdf mostly working
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-08 11:31:26 +00:00
Andy Miller
076c10d34b prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-06 18:54:16 +00:00
Andy Miller
2d75649a08 removed check causing false positives
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-06 18:36:50 +00:00
Andy Miller
c8acc9a499 has some legit uses - this is actually causing problems
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-06 18:36:27 +00:00
Andy Miller
af499184ea update clean commant
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 23:37:26 +00:00
Andy Miller
ebac0a082c prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 23:26:39 +00:00
Andy Miller
4d31bbb43a ignore .github and .phan folders, fixed path check
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 23:25:49 +00:00
Andy Miller
be20cf2e2c prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 21:24:47 +00:00
Andy Miller
c33a1f57bc don’t copy non-upgrade root folders
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 21:23:11 +00:00
Andy Miller
83817428c7 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 18:35:45 +00:00
Andy Miller
d2970a92b5 more safe upgrade fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-05 18:30:24 +00:00
Andy Miller
7b1bcf7789 sync regex fix
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-04 14:15:05 +00:00
Andy Miller
44bdd1283d add preflight command and —safe and —legacy for self-upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-04 14:02:38 +00:00
Andy Miller
32dafbb1cb cache fallbacks for unsupported drivers
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-04 13:57:22 +00:00
Andy Miller
e622326285 improved js assets pipline handling to support defer
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-03 23:48:50 +00:00
Andy Miller
d0287043c2 prepare for release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-31 19:13:57 +00:00
Andy Miller
6c5b801c6f test update script
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-31 19:08:10 +00:00
Andy Miller
460bf241a5 safe upgrae improvements
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-31 18:02:06 +00:00
Andy Miller
ee179e19e5 strict types
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-28 17:40:43 +00:00
Andy Miller
3618a129df bring pimple ‘in-house’ for continued development
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-28 12:22:09 +00:00
Andy Miller
787146cc2c register_argc_argv fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-27 13:17:43 -06:00
Andy Miller
a1fe19f465 replace doctrine/cache with symfony/cache
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-27 12:31:15 -06:00
Andy Miller
f2c26c116a 8.5 not available yet
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-27 11:56:34 -06:00
Andy Miller
d1d70c4d0c set PHP minimum to 8.3
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-27 10:37:43 -06:00
Andy Miller
e5a659d445 fix for PHP 8.5+
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-27 10:31:29 -06:00
Andy Miller
39c4ecfe6a prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-23 09:01:01 -06:00
Andy Miller
3e3aa00a1b rework monolog shim for better compatibility
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-23 08:58:58 -06:00
Andy Miller
9c2497460b don’t crash if getManifest is not available
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-23 08:16:08 -06:00
Andy Miller
f2f58d11d6 vendor updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-21 14:05:36 -06:00
Andy Miller
2d8be2f859 fix for recovery window/manifest via bin/gpm
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-20 13:21:54 -06:00
Andy Miller
f6c57a44de prepare for beta upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-20 11:38:17 -06:00
Andy Miller
0d2d0bdc11 Solution for handling Event errors on upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-20 11:30:44 -06:00
Andy Miller
e110701079 force +x permssions on grav/* via CLI upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-20 11:30:17 -06:00
Andy Miller
c10acd1837 fix for filterFunc and mapFunc
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-20 11:14:33 -06:00
Andy Miller
f9f3b9a8ba support labels in recovery mode
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 21:44:29 -06:00
Andy Miller
e5b7449483 updated .gitignore
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 21:26:31 -06:00
Andy Miller
7077b0b71a fix recovery mode
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 21:09:43 -06:00
Andy Miller
57a446862f jump into recovery mode
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 20:49:02 -06:00
Andy Miller
b2f2e7bd45 more recovery manage fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 18:37:12 -06:00
Andy Miller
3fbd6771e9 more recovery fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 18:18:20 -06:00
Andy Miller
8a10d6bc54 fixing tests
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 18:05:48 -06:00
Andy Miller
0bdde9dec2 Merge branch '1.8' of github.com:getgrav/grav into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 17:59:58 -06:00
Andy Miller
348fa04c47 recovery/command fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 17:59:17 -06:00
Andy Miller
52f0d5f1d7 more fixes for recovery.window and recovery.flag
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 17:33:44 -06:00
Andy Miller
9c6111c368 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 16:15:07 -06:00
Andy Miller
9806533f56 remove plan document
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 16:03:16 -06:00
Andy Miller
e30245789c move recover.flag
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-19 16:02:51 -06:00
Andy Miller
20b95c4585 ignore unpublished plugins - part 2 2025-10-19 11:13:16 -06:00
Andy Miller
6d0fc78462 ignore unpublished plugins 2025-10-19 11:13:15 -06:00
Andy Miller
5420ca2200 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 20:34:38 -06:00
Andy Miller
942f523f18 fix test
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 19:09:59 -06:00
Andy Miller
c812def317 better label handling for snapshots
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 19:07:23 -06:00
Andy Miller
9b2d352f8a more restore bin fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 19:07:23 -06:00
Andy Miller
d932875e66 create adhoc snapshot
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 19:07:23 -06:00
Andy Miller
7a2c151a4b run / restore feature
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-18 19:07:23 -06:00
Andy Miller
81b0f0ec04 bin/restore enhancement
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 22:19:49 -06:00
Andy Miller
70ddb549b7 stop cache clearing snapshots
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 21:08:42 -06:00
Andy Miller
be3cb77f28 more refactoring of safe install
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 20:48:55 -06:00
Andy Miller
345b5e9577 filter out extra folders
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 19:55:55 -06:00
Andy Miller
e88f38bd10 Optimized staged package
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 19:35:43 -06:00
Andy Miller
bdc06afea2 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 18:38:29 -06:00
Andy Miller
f9348a4d9d Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 18:35:14 -06:00
Andy Miller
c9c1267284 ignore recovery file
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 18:16:12 -06:00
Andy Miller
4fa5996414 fix for safe upgrade on 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 18:15:13 -06:00
Andy Miller
c3d1d4ae26 fix for binary permissions in CLI 2025-10-17 11:27:49 -06:00
Andy Miller
c79d2ecfc4 another fix for safe upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 11:11:12 -06:00
Andy Miller
60a97dcf56 test fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 10:14:22 -06:00
Andy Miller
679a6db61d prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-17 10:08:33 -06:00
Andy Miller
b70ae844a8 route safeupgrade status 2025-10-16 23:31:38 -06:00
Andy Miller
e6de9db77e preserver root files 2025-10-16 23:28:23 -06:00
Andy Miller
42e37c1d02 ensureJobResult
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 21:30:05 -06:00
Andy Miller
e764d2ce1c more safeupgrade logic
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 18:51:16 -06:00
Andy Miller
f711cb3208 fixes for permission retention
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 15:25:01 -06:00
Andy Miller
6751d28839 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 12:09:49 -06:00
Andy Miller
8118d6b980 source fix in restore bin + missing dot files after upgrade
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 12:01:48 -06:00
Andy Miller
ba2536136b prepare for release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 10:42:33 -06:00
Andy Miller
ee49305053 timelimit on recovery status
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 09:08:32 -06:00
Andy Miller
b4d664fcb0 built in composer update
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 08:23:22 -06:00
Andy Miller
7fcb1d1cb7 renamed to bin/restore
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 08:09:02 -06:00
Andy Miller
dbeaa8ad46 remove accidental recovery flag + add functionality in grav-restore
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 08:07:56 -06:00
Andy Miller
a3da588829 should fix tests this time
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 07:54:44 -06:00
Andy Miller
a3387c106b more test fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 07:50:24 -06:00
Andy Miller
d9d241d806 fix for RecoveryManagerTest
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-16 07:47:50 -06:00
Andy Miller
bb5cdad333 require grav 1.7.50
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-15 20:55:50 -06:00
Andy Miller
44f90cbce0 Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-15 20:22:14 -06:00
Andy Miller
cc97e2ff45 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-14 14:30:24 -06:00
Andy Miller
d92c430b8a updated changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-14 14:30:01 -06:00
Andy Miller
184cdea75d Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-13 14:39:29 -06:00
Andy Miller
7b9567ec28 update vendor libs
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-13 14:35:58 -06:00
Andy Miller
9e84d5d004 more fixes for Symfony7 PHP 8+
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-07 13:30:40 -06:00
Andy Miller
fd0d3dc463 PHP 8.4 fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-07 13:18:04 -06:00
Andy Miller
eb985e875d vendor updates and some fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-03 18:16:40 -06:00
Andy Miller
ba3493adce vendor update
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-10-03 16:13:05 -06:00
Andy Miller
d785042a0d set YamlUpdater get/set to public
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 15:20:53 -06:00
Andy Miller
49096b61f3 fixed sessions to use 1.7 style..
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 15:12:38 -06:00
Andy Miller
70e986074c prepare beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 14:06:13 -06:00
Andy Miller
4af22edd36 add missing YamlLinter::exists() method
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 14:05:32 -06:00
Andy Miller
5bc7d6943f added cache check interval
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 11:24:50 -06:00
Andy Miller
8eb4085bcd opcache improvements for first hit
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 11:02:12 -06:00
Andy Miller
b47758e3c7 tweaked changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 10:26:04 -06:00
Andy Miller
972ec26035 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-22 10:20:44 -06:00
Andy Miller
9116079e97 twig3 compatiblity layer
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-21 11:40:23 -06:00
Andy Miller
51ddb3984c rector casting clarity
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 22:49:21 -06:00
Andy Miller
22de638e52 composer updates + php fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 22:33:56 -06:00
Andy Miller
365ab93e7e PHP 8.2+ fixes
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 22:12:55 -06:00
Andy Miller
c172964025 fix for cache blowing up
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 19:16:12 -06:00
Andy Miller
cb0bbcdb8b Deferred support in Twig 3
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 18:38:27 -06:00
Andy Miller
35f5d2f329 Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-09-20 17:57:14 -06:00
Andy Miller
639be5ac0d pages optimizations
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-06-25 16:17:24 -06:00
Andy Miller
d3e32799ab use forked copy of parsedown 1.7.x for PHP 8.4 compatibility
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-04-02 05:34:07 -06:00
Andy Miller
3bebfc6dac fixed for tests
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-04-01 17:03:42 -06:00
Andy Miller
9d80a4d992 upgraded to latest phpdebugbar + fixed browser caching
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-04-01 16:55:29 -06:00
Andy Miller
fc7f72f89d Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-03-31 19:32:54 -06:00
Andy Miller
53391466b0 prepare for beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-01-27 14:08:11 -07:00
Andy Miller
2fadc14c01 Swtiched to forked getgrav/Twig 2.x with PHP 8.4 support
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-01-27 14:03:31 -07:00
Andy Miller
de2af9e470 Fix empty string causing parse error: fixes #3894
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-01-24 15:22:35 -07:00
Andy Miller
edfb1a0868 Merge branch 'develop' into 1.8 2025-01-21 14:41:54 +00:00
Andy Miller
9893a605a6 composer update
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-01-20 11:41:51 +00:00
Andy Miller
83d291c24b 8.2 stuff
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-01-14 15:55:10 +00:00
Andy Miller
ca1b05ba57 Merge branch 'develop' into 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>

# Conflicts:
#	system/src/Grav/Framework/Collection/AbstractLazyCollection.php
2025-01-06 14:17:08 +00:00
Andy Miller
350e4c04cd test action on 1.8
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-10 14:35:05 -07:00
Andy Miller
d8123a3662 switch to cache@v4 + limit PHP versions
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-10 14:33:29 -07:00
Andy Miller
1e430f635e some more composer updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-09 17:32:00 -07:00
Andy Miller
17548131d3 rector fixes for PHP 8.2+
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-09 17:23:56 -07:00
Andy Miller
4a27bd780c vendor updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-09 17:14:29 -07:00
Andy Miller
5666d0f211 changelog updated
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-03 15:33:10 -07:00
Andy Miller
d17ab9e06c fixed issue with Abstract Lazy Collection
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-12-03 15:33:03 -07:00
Andy Miller
a15fe29f43 update version #
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-11-21 12:46:31 -07:00
Andy Miller
3126fa8388 updated changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-11-21 12:43:32 -07:00
Andy Miller
b0c339c9eb moved to stable version of clockwork (v5.3.1)
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-11-21 12:41:56 -07:00
Andy Miller
f812ee8555 more compoer updates
Signed-off-by: Andy Miller <rhuk@mac.com>
2024-11-18 16:13:08 -07:00
Andy Miller
8c6388bb74 package update 2024-10-30 19:26:04 -06:00
Andy Miller
d7aaef986e updated version 2024-10-28 11:43:14 +00:00
Andy Miller
ab9363c478 Merge branch 'develop' into 1.8 2024-10-28 11:32:43 +00:00
Andy Miller
7f2da96c0b update vendor libs 2024-10-28 11:31:20 +00:00
Andy Miller
b16db4b9c0 typo 2024-10-27 12:21:00 +00:00
Andy Miller
42c0682dd8 Merge branch '1.8' of github.com:getgrav/grav into 1.8 2024-10-26 15:25:11 +01:00
Andy Miller
1969ec1876 monolog2 support 2024-10-26 15:25:00 +01:00
Jeremy Gonyea
c30661736c Fix for #3164. Adds aliases as possible commands during lookup (#3863) 2024-10-26 15:18:43 +01:00
Andy Miller
3d2dfa2faf Updated changelog 2024-10-25 20:18:33 +01:00
Andy Miller
f06bbfc563 Missing RocketTheme\Toolbox\Event\EventSubscriberInterface 2024-10-25 20:17:41 +01:00
Andy Miller
37e5526a4f updated changelog 2024-10-25 18:21:23 +01:00
Andy Miller
85c4b8279e fixes for PHP 8.4 - Implicitly nullable parameter declarations deprecated 2024-10-25 18:20:41 +01:00
Andy Miller
46736ce256 upgrade to doctrine/collectons 2.2 2024-10-25 17:53:51 +01:00
Andy Miller
800b2e1ecb updated changelog 2024-10-25 17:39:34 +01:00
Andy Miller
4f065b95a7 updated composer to latest version 2024-10-25 17:33:47 +01:00
Andy Miller
b59a3adc80 avif support via getgrav/image updates 2024-10-25 15:29:16 +01:00
Andy Miller
4ec9a3a489 Merge branch 'develop' into 1.8 2024-10-25 10:49:38 +01:00
Andy Miller
173d08243a use dev-master of Clockwork to support Monolog2/3 2024-10-24 11:58:04 +01:00
Andy Miller
62c60b8ba1 Fixed a couple of monolog deprecations 2024-10-23 17:17:51 +01:00
Andy Miller
59031a8711 use BUILD_SCRIPT_URL_18 in build 2024-10-23 17:07:10 +01:00
Andy Miller
5cd859865f use build-grav-18.sh build script 2024-10-23 16:44:49 +01:00
Andy Miller
16eafbbb04 udpate build.yaml 2024-10-23 14:37:45 +01:00
Andy Miller
3947bb03aa set version properly 2024-10-23 13:14:26 +01:00
Andy Miller
ee55d097f2 set testing true 2024-10-23 13:08:34 +01:00
Andy Miller
ae567469b7 prepare for beta release 2024-10-23 13:08:06 +01:00
Andy Miller
f6decaab15 Merge branch 'develop' into 1.8 2024-10-23 13:04:01 +01:00
Andy Miller
10d36a10bc updated composer.lock 2024-10-23 12:16:29 +01:00
Andy Miller
574a430a10 more composer updates 2024-10-23 12:14:45 +01:00
Andy Miller
302f02ca5d revert schema 2024-10-22 18:01:41 +01:00
Andy Miller
c334479e4c Merge branch 'develop' into feature/v1.8 2024-10-22 15:47:58 +01:00
Andy Miller
b96483c49a Fix tests for latest Codeception version 2024-10-22 13:24:35 +01:00
Andy Miller
cccce836f6 Merge branch 'develop' into feature/v1.8 2024-10-22 12:09:26 +01:00
Andy Miller
bdcb77d429 Merge branch 'develop' into feature/v1.8 2024-10-21 14:20:23 +01:00
Andy Miller
e15cc86716 toAscii() should be static 2024-10-21 14:19:45 +01:00
Andy Miller
dcfbd73d43 twig compatibility stuff 2024-10-14 20:53:07 +01:00
Andy Miller
1967910789 upgrade to latest symfony 6.4 + PHP 8.2 2024-10-14 19:27:43 +01:00
Andy Miller
4f1f9a7755 Merge branch 'develop' into feature/v1.8
# Conflicts:
#	CHANGELOG.md
#	composer.lock
2024-10-14 19:25:17 +01:00
Andy Miller
e4f483998d composer lock file 2023-05-12 13:50:50 -06:00
Andy Miller
7a393101ee Merge branch 'develop' into feature/v1.8
# Conflicts:
#	CHANGELOG.md
#	composer.json
#	composer.lock
#	system/blueprints/config/system.yaml
#	system/src/Grav/Common/Twig/WriteCacheFileTrait.php
2023-05-12 13:44:07 -06:00
Matias Griese
10b15bedf2 Composer update 2022-08-18 13:27:58 +03:00
Matias Griese
8c14a9907e Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.lock
2022-08-18 13:27:06 +03:00
Matias Griese
f490d9a7e1 Fix phpstan error 2022-06-29 13:16:56 +03:00
Matias Griese
81ca0c2e25 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	CHANGELOG.md
	composer.json
	composer.lock
2022-06-29 13:14:06 +03:00
Matias Griese
fbfac9f8f4 Composer update 2022-06-29 13:12:11 +03:00
Matias Griese
d6e72708bf Composer update 2022-06-15 14:41:05 +03:00
Matias Griese
2aff274c31 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.lock
	system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php
2022-06-15 14:39:38 +03:00
Matias Griese
74c1dfa433 Composer update 2022-05-20 16:48:41 +03:00
Matias Griese
f51a9d9d87 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	CHANGELOG.md
	composer.lock
2022-05-20 16:45:54 +03:00
Matias Griese
e5498f58e6 Composer update 2022-04-19 11:05:19 +03:00
Matias Griese
8f58a4494c Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.lock
2022-04-19 11:04:33 +03:00
Matias Griese
5bfd43256d Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8 2022-03-31 11:31:04 +03:00
Matias Griese
69bc3f7f25 Composer update 2022-03-31 11:27:39 +03:00
Matias Griese
782ceada80 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	CHANGELOG.md
	composer.lock
2022-03-31 11:26:37 +03:00
Matias Griese
36392acbea Composer update 2022-03-23 13:00:19 +02:00
Matias Griese
4b564df38f Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	CHANGELOG.md
2022-03-23 12:58:48 +02:00
Matias Griese
9d179e5b2a Composer update 2022-03-09 12:33:20 +02:00
Matias Griese
6032bd07dc Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.json
	composer.lock
2022-03-09 12:32:31 +02:00
Matias Griese
9a87a509b0 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8 2022-02-22 11:35:29 +02:00
Matias Griese
46f2a81b21 Phpstan fixes 2022-02-19 15:20:06 +02:00
Matias Griese
f5e21645f6 Support phpstan level 6 in Framework classes 2022-02-19 14:17:02 +02:00
Matias Griese
12c8cf9c40 Phpstan fixes 2022-02-19 13:47:29 +02:00
Matias Griese
93bb929b38 Phpstan updates 2022-02-19 13:17:34 +02:00
Matias Griese
85eaf308d5 Fixed FlexDirectory::getCache() after converting to Symfony Cache 2022-02-19 13:17:19 +02:00
Matias Griese
d9ede28b99 Composer update 2022-02-19 12:18:40 +02:00
Matias Griese
2e65b0eea4 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.lock
2022-02-19 12:18:00 +02:00
Matias Griese
7660a80ef7 Use older monolog due to clockwork incompatibility 2022-02-16 09:51:09 +02:00
Matias Griese
6d0a436834 Changelog update (pt 2) 2022-02-12 16:58:07 +02:00
Matias Griese
176fd8f1d8 Changelog update 2022-02-12 16:52:25 +02:00
Matias Griese
2f85fe9c99 Move unmaintained/unused vendor dependencies to the end of the list 2022-02-12 16:49:09 +02:00
Matias Griese
d93d297dc4 Updated to **Monolog 2.3** 2022-02-12 16:36:58 +02:00
Matias Griese
fcd9093f84 Use **Symfony Cache** instead of unmaintaided **Doctrine Cache** (with backward compatibility layer) 2022-02-12 15:33:38 +02:00
Matias Griese
58b54a70bd Removed system.umask_fix setting for security reasons 2022-02-12 13:49:30 +02:00
Matias Griese
9daa0a9041 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.lock
2022-02-12 13:08:23 +02:00
Matias Griese
965e03daf2 Update dependencies so that everything works again 2022-02-07 14:14:11 +02:00
Matias Griese
b6c3db082a Update composer dependencies 2022-02-07 14:03:05 +02:00
Matias Griese
26b68953c4 Composer update 2022-02-07 13:48:47 +02:00
Matias Griese
fba015c5d9 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.json
	composer.lock
2022-02-07 13:47:41 +02:00
Matias Griese
7223c177c4 Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.json
	composer.lock
2021-12-08 10:08:34 +02:00
Matias Griese
a68bdd2b75 Fixed Toolbox Event 2021-11-25 14:17:13 +02:00
Matias Griese
38e4624506 Composer update 2021-11-25 11:51:40 +02:00
Matias Griese
ad9287ee0f Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8
 Conflicts:
	composer.json
	composer.lock
2021-11-25 11:46:21 +02:00
Matias Griese
777ac119de Update all libraries (WIP) 2021-10-20 20:33:05 +03:00
Matias Griese
076c64841e Merge branch 'develop' of github.com:getgrav/grav into feature/v1.8 2021-10-20 20:28:50 +03:00
Matias Griese
62cb40fef5 Composer update 2021-10-20 20:28:24 +03:00
360 changed files with 12956 additions and 6700 deletions

View File

@@ -11,11 +11,13 @@ jobs:
permissions:
contents: write # for release creation (svenstaro/upload-release-action)
if: "!github.event.release.prerelease"
#if: "!github.event.release.prerelease"
runs-on: ubuntu-latest
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: 7.3
php-version: 8.3
extensions: opcache, gd
tools: composer:v2
coverage: none
@@ -38,10 +40,10 @@ jobs:
- name: Retrieval of Builder Scripts
run: |
# Real Grav URL
curl --silent -H "Authorization: token ${{ secrets.GLOBAL_TOKEN }}" -H "Accept: application/vnd.github.v3.raw" ${{ secrets.BUILD_SCRIPT_URL }} --output build-grav.sh
curl --silent -H "Authorization: token ${{ secrets.GLOBAL_TOKEN }}" -H "Accept: application/vnd.github.v3.raw" ${{ secrets.BUILD_SCRIPT_URL_18 }} --output build-grav.sh
# Development Local URL
# curl ${{ secrets.BUILD_SCRIPT_URL }} --output build-grav.sh
# curl ${{ secrets.BUILD_SCRIPT_URL_18 }} --output build-grav.sh
- name: Grav Builder
run: |

View File

@@ -2,9 +2,9 @@ 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)
@@ -13,7 +13,7 @@ jobs:
unit-tests:
strategy:
matrix:
php: ['8.3', '8.2', '8.1', '8.0', '7.4', '7.3']
php: [8.5, 8.4, 8.3]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
@@ -46,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

View File

@@ -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)

3
.gitignore vendored
View File

@@ -48,5 +48,6 @@ tests/cache/*
tests/error.log
system/templates/testing/*
/user/config/versions.yaml
/system/recovery.window
/user/data/recovery.window
tmp/*
/AGENTS.md

View File

@@ -1,3 +1,214 @@
# 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
1. [](#new)
* Use `dev-master` branch of Clockwork to support Monolog2 / Monolog3
* `AVIF` image support via updates to `getgrav/Image` library
* Upgraded to **Doctrine Collection 2.2**
1. [](#improved)
* Updated composer libraries
* Updated composer.php binary to `v2.8.1`
* Fixes for PHP 8.4 - Implicitly nullable parameter declarations deprecated
* Added back Missing `RocketTheme\Toolbox\Event\EventSubscriberInterface` for Gantry5
1. [](#bugfix)
* Various fixes to use `$log->debug()`, `$log->info()`, `$log->warning()` and `$log->error()` For Monolog2 support
# v1.8.0-beta.1
## 10/23/2024
1. [](#new)
* Set minimum requirements to **PHP 8.3**
* Updated to **Twig 2.14**
* Updated to **Symfony 6.4**
* Updated to **Monolog 2.3**
* Updated to **RocketTheme/Toolbox 2.0**
* Updated to **Composer/Semver 3.2**
* Use **Symfony Cache** instead of unmaintained **Doctrine Cache**
* Removed unsupported **APC**, **WinCache**, **XCache** and **Memcache**, use apcu or memcached instead
* Removed `system.umask_fix` setting for security reasons
* Support phpstan level 6 in Framework classes
# v1.7.50
## UNRELEASED
@@ -72,18 +283,18 @@
## 10/23/2024
1. [](#new)
* New `Utils::toAscii()` method
* Added support for Clockwork Debugger to allow web UI (requires new `clockwork-web` plugin)
* New `Utils::toAscii()` method
* Added support for Clockwork Debugger to allow web UI (requires new `clockwork-web` plugin)
1. [](#improved)
* Include modular sub-pages in last-modification date computation [#3562](https://github.com/getgrav/grav/pull/3562)
* Updated vendor libs to latest versions
* Updated JQuery to `3.7.1` [#3787](https://github.com/getgrav/grav/pull/3827)
* Updated vendor libraries to latest versions
* Support for Fediverse Creator meta tag [#3844](https://github.com/getgrav/grav/pull/3844)
* Include modular sub-pages in last-modification date computation [#3562](https://github.com/getgrav/grav/pull/3562)
* Updated vendor libs to latest versions
* Updated JQuery to `3.7.1` [#3787](https://github.com/getgrav/grav/pull/3827)
* Updated vendor libraries to latest versions
* Support for Fediverse Creator meta tag [#3844](https://github.com/getgrav/grav/pull/3844)
1. [](#bugfix)
* Fixes deprecated for return type in Filesystem with PHP 8.3.6 [#3831](https://github.com/getgrav/grav/issues/3831)
* Fix for `exif_imagtetype()` throwing an exception when file doesn't exist
* Fix JSON output comments check with content type [#3859](https://github.com/getgrav/grav/pull/3859)
* Fixes deprecated for return type in Filesystem with PHP 8.3.6 [#3831](https://github.com/getgrav/grav/issues/3831)
* Fix for `exif_imagtetype()` throwing an exception when file doesn't exist
* Fix JSON output comments check with content type [#3859](https://github.com/getgrav/grav/pull/3859)
# v1.7.46
## 05/15/2024

View File

@@ -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
@@ -89,9 +89,10 @@ bin/gpm update
## Upgrading from older version
* [Upgrading to Grav 1.8](https://learn.getgrav.org/16/advanced/grav-development/grav-18-upgrade-guide)
* [Upgrading to Grav 1.7](https://learn.getgrav.org/16/advanced/grav-development/grav-17-upgrade-guide)
* [Upgrading to Grav 1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-16-upgrade-guide)
* [Upgrading from Grav <1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-15-upgrade-guide)
* [Upgrading from Grav before 1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-15-upgrade-guide)
# Contributing
We appreciate any contribution to Grav, whether it is related to bugs, grammar, or simply a suggestion or improvement! Please refer to the [Contributing guide](CONTRIBUTING.md) for more guidance on this topic.

209
bin/build-test-update.php Executable file
View 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.

View File

@@ -25,6 +25,7 @@ if (!file_exists($root . '/index.php')) {
exit(1);
}
use Grav\Common\Filesystem\Folder;
use Grav\Common\Recovery\RecoveryManager;
use Grav\Common\Upgrade\SafeUpgradeService;
use Symfony\Component\Yaml\Yaml;
@@ -39,16 +40,24 @@ Usage:
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;
@@ -61,17 +70,35 @@ function parseArguments(array $args): array
{
array_shift($args); // remove script name
$command = $args[0] ?? 'help';
$command = null;
$arguments = [];
$options = [];
foreach (array_slice($args, 1) as $arg) {
if (substr($arg, 0, 2) === '--') {
echo "Unknown option: {$arg}\n";
exit(1);
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;
}
$arguments[] = $arg;
if (null === $command) {
$command = $arg;
} else {
$arguments[] = $arg;
}
}
if (null === $command) {
$command = 'interactive';
}
return [
@@ -81,22 +108,23 @@ function parseArguments(array $args): array
];
}
/**
* @return string|null
*/
/**
* @param array $options
* @return SafeUpgradeService
*/
function createUpgradeService(array $options): SafeUpgradeService
{
$options['root'] = GRAV_ROOT;
$serviceOptions = ['root' => GRAV_ROOT];
return new SafeUpgradeService($options);
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,source_version:?string,target_version:?string,created_at:int}>
* @return list<array{id:string,label:?string,source_version:?string,target_version:?string,created_at:int}>
*/
function loadSnapshots(): array
{
@@ -117,21 +145,355 @@ function loadSnapshots(): array
$snapshots[] = [
'id' => $decoded['id'],
'label' => $decoded['label'] ?? null,
'source_version' => $decoded['source_version'] ?? null,
'target_version' => $decoded['target_version'] ?? null,
'created_at' => $decoded['created_at'] ?? 0,
'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) {
@@ -141,12 +503,60 @@ switch ($command) {
echo "Available snapshots:\n";
foreach ($snapshots as $snapshot) {
$time = $snapshot['created_at'] ? date('c', (int)$snapshot['created_at']) : 'unknown';
$restoreVersion = $snapshot['source_version'] ?? $snapshot['target_version'] ?? 'unknown';
echo sprintf(" - %s (restore to Grav %s, %s)\n", $snapshot['id'], $restoreVersion, $time);
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) {
@@ -154,22 +564,12 @@ switch ($command) {
exit(1);
}
try {
$service = createUpgradeService($options);
$manifest = $service->rollback($snapshotId);
} catch (\Throwable $e) {
fwrite(STDERR, "Restore failed: " . $e->getMessage() . "\n");
exit(1);
}
applySnapshot($snapshotId, $options);
break;
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";
exit(0);
case 'snapshot':
createManualSnapshot($options);
break;
case 'recovery':
$action = strtolower($arguments[0] ?? 'status');

View File

@@ -2,7 +2,7 @@ actor: Tester
bootstrap: _bootstrap.php
paths:
tests: tests
log: tests/_output
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs

View File

@@ -12,7 +12,7 @@
"homepage": "https://getgrav.org",
"license": "MIT",
"require": {
"php": "^7.3.6 || ^8.0",
"php": "^8.3",
"ext-json": "*",
"ext-openssl": "*",
"ext-curl": "*",
@@ -20,61 +20,78 @@
"ext-dom": "*",
"ext-libxml": "*",
"ext-gd": "*",
"symfony/polyfill-mbstring": "~1.23",
"symfony/polyfill-iconv": "^1.23",
"symfony/polyfill-php74": "^1.23",
"symfony/polyfill-php80": "^1.23",
"symfony/polyfill-php81": "^1.23",
"psr/simple-cache": "^1.0",
"psr/http-message": "^1.0",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-iconv": "^1.24",
"symfony/polyfill-php80": "^1.24",
"symfony/polyfill-php81": "^1.24",
"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.0",
"nyholm/psr7-server": "^1.0",
"nyholm/psr7": "^1.3",
"twig/twig": "~v1.44",
"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",
"nyholm/psr7-server": "^1.1",
"nyholm/psr7": "^1.8",
"erusev/parsedown": "^1.7",
"erusev/parsedown-extra": "~0.8",
"symfony/contracts": "~1.1",
"symfony/yaml": "~4.4",
"symfony/console": "~4.4",
"symfony/event-dispatcher": "~4.4",
"symfony/var-dumper": "~4.4",
"symfony/process": "~4.4",
"doctrine/cache": "^1.10",
"doctrine/collections": "^1.6",
"guzzlehttp/psr7": "^1.7",
"filp/whoops": "~2.9",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.25",
"rockettheme/toolbox": "v2.x-dev",
"composer/ca-bundle": "^1.5",
"composer/semver": "^3.4",
"dragonmantank/cron-expression": "^3.3",
"willdurand/negotiation": "^3.1",
"rhukster/dom-sanitizer": "^1.0",
"tubalmartin/cssmin": "^4.1",
"tedivm/jshrink": "^1.7",
"donatj/phpuseragentparser": "~1.9",
"guzzlehttp/psr7": "^2.7",
"filp/whoops": "~2.16",
"itsgoingd/clockwork": "^5.3",
"php-debugbar/php-debugbar": "~2.1",
"getgrav/image": "^4.0",
"getgrav/cache": "^2.0",
"donatj/phpuseragentparser": "~1.1",
"pimple/pimple": "~3.5.0",
"rockettheme/toolbox": "~1.5",
"maximebf/debugbar": "~1.16",
"league/climate": "^3.6",
"antoligy/dom-string-iterators": "^1.0",
"miljar/php-exif": "^0.6",
"composer/ca-bundle": "^1.2",
"dragonmantank/cron-expression": "^3.3",
"willdurand/negotiation": "^3.0",
"itsgoingd/clockwork": "^5.0",
"symfony/http-client": "^4.4",
"composer/semver": "^1.4",
"rhukster/dom-sanitizer": "^1.0",
"league/climate": "^3.10",
"multiavatar/multiavatar-php": "^1.0"
},
"require-dev": {
"codeception/codeception": "^4.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpunit/php-code-coverage": "~9.2",
"codeception/codeception": "^5.1",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/php-code-coverage": "^11.0",
"getgrav/markdowndocs": "^2.0",
"codeception/module-asserts": "^1.3",
"codeception/module-phpbrowser": "^1.0"
"codeception/module-asserts": "*",
"codeception/module-phpbrowser": "*",
"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": {
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*"
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*"
},
"suggest": {
"ext-mbstring": "Recommended for better performance",
@@ -87,12 +104,18 @@
"ext-exif": "Needed to use exif data from images."
},
"config": {
"apcu-autoloader": true
"apcu-autoloader": true,
"platform": {
"php": "8.3"
}
},
"autoload": {
"psr-4": {
"Grav\\": "system/src/Grav",
"Twig\\": "system/src/Twig"
"Doctrine\\": "system/src/Doctrine",
"RocketTheme\\": "system/src/RocketTheme",
"Twig\\": "system/src/Twig",
"Pimple\\": "system/src/Pimple"
},
"files": [
"system/defines.php",
@@ -111,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 5 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
"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": {

3985
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,8 @@
namespace Grav;
\define('GRAV_REQUEST_TIME', microtime(true));
\define('GRAV_PHP_MIN', '7.3.6');
if (!\defined('GRAV_ROOT')) {
\define('GRAV_ROOT', __DIR__);
}
\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;
@@ -24,6 +22,12 @@ 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) ?? '/';
@@ -39,6 +43,10 @@ if (PHP_SAPI !== 'cli') {
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;
@@ -62,18 +70,6 @@ if (!is_file($autoload)) {
// Register the auto-loader.
$loader = require $autoload;
if (!class_exists(\Symfony\Component\ErrorHandler\Exception\FlattenException::class, false) && class_exists(\Symfony\Component\HttpKernel\Exception\FlattenException::class)) {
class_alias(\Symfony\Component\HttpKernel\Exception\FlattenException::class, \Symfony\Component\ErrorHandler\Exception\FlattenException::class);
}
if (!class_exists(\Monolog\Logger::class, false)) {
class_exists(\Monolog\Logger::class);
}
if (defined('Monolog\Logger::API') && \Monolog\Logger::API < 3) {
require_once __DIR__ . '/system/src/Grav/Framework/Compat/Monolog/bootstrap.php';
}
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());
@@ -81,7 +77,7 @@ date_default_timezone_set(@date_default_timezone_get());
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
$recoveryFlag = __DIR__ . '/system/recovery.flag';
$recoveryFlag = __DIR__ . '/user/data/recovery.flag';
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag)) {
require __DIR__ . '/system/recovery.php';
return 0;
@@ -91,12 +87,18 @@ 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)) {
require __DIR__ . '/system/recovery.php';
return 0;
}
throw $e;
}

505
needs_fixing.txt Normal file
View 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

View File

@@ -1,48 +0,0 @@
# Grav Safe Self-Upgrade Prototype
This document tracks the design decisions behind the new self-upgrade prototype for Grav 1.8.
## Goals
- Prevent in-place mutation of the running Grav tree.
- Guarantee a restorable snapshot before any destructive change.
- Detect high-risk plugin incompatibilities (eg. `psr/log`) prior to upgrading.
- Provide a recovery surface that does not depend on a working Admin plugin.
## High-Level Flow
1. **Preflight**
- Ensure PHP & extensions satisfy the target release requirements.
- Refresh GPM metadata and require all plugins/themes to be on their latest compatible release.
- Scan plugin `composer.json` files for dependencies that are known to break under Grav 1.8 (eg. `psr/log` < 3) and surface actionable warnings.
2. **Stage**
- Download the Grav update archive into a staging area (`tmp://grav-snapshots/{timestamp}`).
- Extract the package, then write a manifest describing the target version, PHP info, and enabled packages.
- Snapshot the live `user/` directory and relevant metadata into the same stage folder.
3. **Promote**
- Copy the staged package into place, overwriting Grav core files while leaving hydrated user content intact.
- Clear caches in the staged tree before promotion.
- Run Grav CLI smoke checks (`bin/grav check`) while still holding maintenance state; restore from the snapshot automatically on failure.
4. **Finalize**
- Record the manifest under `user/data/upgrades`.
- Resume normal traffic by removing the maintenance flag.
- Leave the previous tree and manifest available for manual rollback commands.
## Recovery Mode
- Introduce a `system/recovery.flag` sentinel written whenever a fatal error occurs during bootstrap or when a promoted release fails validation.
- While the flag is present, Grav forces a minimal Recovery UI served outside of Admin, protected by a short-lived signed token.
- The Recovery UI lists recent manifests, quarantined plugins, and offers rollback/disabling actions.
- Clearing the flag requires either a successful rollback or a full Grav request cycle without fatal errors.
## CLI Additions
- `bin/gpm preflight grav@<version>`: runs the same preflight checks without executing the upgrade.
- `bin/gpm rollback [<manifest-id>]`: swaps the live tree with a stored rollback snapshot.
- Existing `self-upgrade` command now wraps the stage/promote pipeline and respects the snapshot manifest.
## Open Items
- Finalize compatibility heuristics (initial pass focuses on `psr/log` and removed logging APIs).
- UX polish for the Recovery UI (initial prototype will expose basic actions only).
- Decide retention policy for old manifests and snapshots (prototype keeps the most recent three).

View File

@@ -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;
}

View File

@@ -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
@@ -619,11 +628,9 @@ form:
options:
auto: Auto detect
file: File
apc: APC
apcu: APCu
memcache: Memcache
memcached: Memcached
wincache: WinCache
redis: Redis
cache.prefix:
@@ -720,20 +727,6 @@ form:
validate:
type: bool
cache.memcache.server:
type: text
size: medium
label: PLUGIN_ADMIN.MEMCACHE_SERVER
help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP
placeholder: "localhost"
cache.memcache.port:
type: text
size: small
label: PLUGIN_ADMIN.MEMCACHE_PORT
help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP
placeholder: "11211"
cache.memcached.server:
type: text
size: medium
@@ -791,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
@@ -809,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
@@ -897,17 +890,6 @@ form:
validate:
type: bool
twig.umask_fix:
type: toggle
label: PLUGIN_ADMIN.TWIG_UMASK_FIX
help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
assets:
type: tab
title: PLUGIN_ADMIN.ASSETS
@@ -1614,6 +1596,15 @@ form:
validate:
type: bool
updates.safe_upgrade_snapshot_limit:
type: number
label: PLUGIN_ADMIN.SAFE_UPGRADE_SNAPSHOT_LIMIT
help: PLUGIN_ADMIN.SAFE_UPGRADE_SNAPSHOT_LIMIT_HELP
default: 5
validate:
type: int
min: 0
http_section:
type: section
title: PLUGIN_ADMIN.HTTP_SECTION
@@ -1799,8 +1790,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
@@ -1833,8 +1824,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
@@ -1854,7 +1845,7 @@ form:
validate:
type: bool
strict_mode.twig_compat:
strict_mode.twig2_compat:
type: toggle
label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
highlight: 0
@@ -1866,6 +1857,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

File diff suppressed because it is too large Load Diff

View File

@@ -93,13 +93,14 @@ cache:
enabled: true # Set to true to enable caching
check:
method: file # Method to check for updates in pages: file|folder|hash|none
driver: auto # One of: auto|file|apcu|memcache|wincache
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)
clear_at: '0 3 * * *' # How often to clear cache (using new scheduler)
clear_job_type: 'standard' # Type to clear when processing the scheduled clear job `standard`|`all`
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, memcache, etc.)
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
@@ -118,7 +119,6 @@ twig:
undefined_filters: true # Allow undefined filters
safe_functions: [] # List of PHP functions which are allowed to be used as Twig functions
safe_filters: [] # List of PHP functions which are allowed to be used as Twig filters
umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775
assets: # Configuration for Assets Manager (JS, CSS)
css_pipeline: false # The CSS pipeline is the unification of multiple CSS resources into one file
@@ -205,6 +205,7 @@ gpm:
updates:
safe_upgrade: true # Enable guarded staging+rollback pipeline for Grav self-updates
safe_upgrade_snapshot_limit: 5 # Maximum number of safe-upgrade snapshots to retain (0 = unlimited)
http:
method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
@@ -234,5 +235,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

View File

@@ -9,13 +9,13 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.50');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);
define('GRAV_VERSION', '1.8.0-beta.25');
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', '7.3.6');
define('GRAV_PHP_MIN', '8.3.0');
}
// Directory separator

View File

@@ -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;

View File

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

View File

@@ -63,21 +63,37 @@ if (is_file($quarantineFile)) {
}
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
$manifests = [];
$snapshots = [];
if (is_dir($manifestDir)) {
$files = glob($manifestDir . '/*.json');
if ($files) {
rsort($files);
foreach ($files as $file) {
$decoded = json_decode(file_get_contents($file), true);
if (is_array($decoded)) {
$decoded['file'] = basename($file);
$manifests[] = $decoded;
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;
header('Content-Type: text/html; charset=utf-8');
?><!doctype html>
@@ -89,7 +105,8 @@ header('Content-Type: text/html; charset=utf-8');
<style>
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 40px; background: #111; color: #eee; }
.panel { max-width: 720px; margin: 0 auto; background: #1d1d1f; padding: 24px 32px; border-radius: 12px; box-shadow: 0 10px 45px rgba(0,0,0,0.4); }
h1 { margin-top: 0; color: #9ef; }
h1 { font-size: 2.5rem; margin-top: 0; color: #fff; display:flex;align-items:center; }
h1 > img {margin-right:1rem;}
code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
form { margin-top: 16px; }
input[type="text"] { width: 100%; padding: 10px; border: 1px solid #333; border-radius: 6px; background: #151517; color: #fff; }
@@ -106,7 +123,7 @@ header('Content-Type: text/html; charset=utf-8');
</head>
<body>
<div class="panel">
<h1>Grav Recovery Mode</h1>
<h1><img src="system/assets/grav.png">Grav Recovery Mode</h1>
<?php if ($notice): ?>
<div class="message notice"><?php echo htmlspecialchars($notice, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
@@ -116,7 +133,7 @@ header('Content-Type: text/html; charset=utf-8');
<?php if (!$authenticated): ?>
<p>This site is running in recovery mode because Grav detected a fatal error.</p>
<p>Locate the recovery token in <code>system/recovery.flag</code> and enter it below.</p>
<p>Locate the recovery token in <code>user/data/recovery.flag</code> and enter it below.</p>
<form method="post">
<input type="hidden" name="action" value="authenticate">
<label for="token">Recovery token</label>
@@ -153,18 +170,22 @@ header('Content-Type: text/html; charset=utf-8');
<div class="card">
<h3>Rollback</h3>
<?php if ($manifests): ?>
<?php if ($latestSnapshot): ?>
<form method="post">
<input type="hidden" name="action" value="rollback">
<label for="manifest">Choose a snapshot</label>
<select id="manifest" name="manifest">
<?php foreach ($manifests as $manifest): ?>
<option value="<?php echo htmlspecialchars($manifest['id'], ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($manifest['id'], ENT_QUOTES, 'UTF-8'); ?> — Grav <?php echo htmlspecialchars($manifest['target_version'] ?? 'unknown', ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="secondary">Rollback to Selected Snapshot</button>
<input type="hidden" name="manifest" value="<?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?>">
<p>
Latest snapshot:
<code><?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?></code>
<?php if (!empty($latestSnapshot['label'])): ?>
<br><small><?php echo htmlspecialchars($latestSnapshot['label'], ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
— Grav <?php echo htmlspecialchars($latestSnapshot['target_version'] ?? 'unknown', ENT_QUOTES, 'UTF-8'); ?>
<?php if (!empty($latestSnapshot['created_at'])): ?>
<br><small>Created <?php echo htmlspecialchars(date('c', (int)$latestSnapshot['created_at']), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</p>
<button type="submit" class="secondary">Rollback to Latest Snapshot</button>
</form>
<?php else: ?>
<p>No upgrade snapshots were found.</p>

16
system/rector.php Normal file
View 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,
]);

View File

@@ -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;
}

View 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();
}

View 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();
}

View 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();
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Doctrine\Common\Cache;
use Grav\Common\Cache\SymfonyCacheProvider;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
/**
* Filesystem cache driver (backwards compatibility).
*/
class FilesystemCache extends SymfonyCacheProvider
{
public const EXTENSION = '.doctrinecache.data';
/**
* @param string $directory
* @param string $extension
* @param int $umask
*/
public function __construct($directory, $extension = self::EXTENSION, $umask = 0002)
{
parent::__construct(new FilesystemAdapter('', 0, $directory));
}
}

View 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();
}

View 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);
}

View 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);
}

View 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
{
}

View 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);
}

View File

@@ -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,18 @@ 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);
$pipeline_output .= $pipeline->$render_pipeline($pipeline_group['assets'], $group, $group_attributes);
}
} else {
foreach ($pipeline_assets as $asset) {
$pipeline_output .= $asset->render();
@@ -577,19 +585,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;
}
}

View File

@@ -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?

View File

@@ -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;
}

View File

@@ -16,8 +16,8 @@ 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 +144,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
@@ -206,9 +205,7 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('js')) {
$minifier = new JS();
$minifier->add($buffer);
$buffer = $minifier->minify();
$buffer = JSMinifier::minify($buffer);
}
// Write file
@@ -283,7 +280,7 @@ class Pipeline extends PropertyObject
} else {
return str_replace($matches[2], $new_url, $matches[0]);
}
}, $file);
}, (string) $file);
return $file;
}

View File

@@ -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)) {

View File

@@ -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);
}

View File

@@ -338,7 +338,7 @@ trait TestingAssetsTrait
$directory,
FilesystemIterator::SKIP_DOTS
)), $pattern);
$offset = strlen($ltrim);
$offset = strlen((string) $ltrim);
$files = [];
foreach ($iterator as $file) {

View File

@@ -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]));
}
@@ -106,7 +106,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 +158,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;
@@ -194,7 +194,7 @@ class Backups
* @param callable|null $status
* @return string|null
*/
public static function backup($id = 0, callable $status = null)
public static function backup($id = 0, ?callable $status = null)
{
$grav = Grav::instance();
@@ -210,7 +210,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;

View File

@@ -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;)");
}
}

View File

@@ -10,15 +10,24 @@
namespace Grav\Common;
use DirectoryIterator;
use \Doctrine\Common\Cache as DoctrineCache;
use Doctrine\Common\Cache\CacheProvider;
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;
@@ -27,12 +36,11 @@ use function is_array;
/**
* The GravCache object is used throughout Grav to store and retrieve cached data.
* It uses DoctrineCache library 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
* MemCache
* MemCacheD
* MemCached
* FileSystem
*/
class Cache extends Getters
@@ -49,7 +57,10 @@ class Cache extends Getters
/** @var Config $config */
protected $config;
/** @var DoctrineCache\CacheProvider */
/** @var AdapterInterface */
protected $adapter;
/** @var CacheProvider */
protected $driver;
/** @var CacheInterface */
@@ -70,6 +81,7 @@ class Cache extends Getters
protected static $standard_remove = [
'cache://twig/',
'cache://doctrine/',
'cache://grav/',
'cache://compiled/',
'cache://clockwork/',
'cache://validated-',
@@ -80,6 +92,7 @@ class Cache extends Getters
protected static $standard_remove_no_images = [
'cache://twig/',
'cache://doctrine/',
'cache://grav/',
'cache://compiled/',
'cache://clockwork/',
'cache://validated-',
@@ -142,14 +155,14 @@ class Cache extends Getters
// Cache key allows us to invalidate all cache on configuration changes.
$this->key = ($prefix ?: 'g') . '-' . $uniqueness;
$this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
$this->cache_dir = $grav['locator']->findResource('cache://grav/' . $uniqueness, true, true);
$this->driver_setting = $this->config->get('system.cache.driver');
$this->driver = $this->getCacheDriver();
$this->driver->setNamespace($this->key);
$this->adapter = $this->getCacheAdapter();
$this->driver = $this->getCacheDriver($this->adapter);
/** @var EventDispatcher $dispatcher */
$dispatcher = Grav::instance()['events'];
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
$dispatcher->addListener('onSchedulerInitialized', $this->onSchedulerInitialized(...));
}
/**
@@ -158,12 +171,7 @@ class Cache extends Getters
public function getSimpleCache()
{
if (null === $this->simpleCache) {
$cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime());
// Disable cache key validation.
$cache->setValidation(false);
$this->simpleCache = $cache;
$this->simpleCache = new Psr16Cache($this->adapter);
}
return $this->simpleCache;
@@ -280,105 +288,185 @@ class Cache extends Getters
* If there is no config option for $driver in the config, or it's set to 'auto', it will
* pick the best option based on which cache extensions are installed.
*
* @return DoctrineCache\CacheProvider The cache driver to use
* @param string|null $namespace
* @param int|null $defaultLifetime
* @return AdapterInterface The cache driver to use
* @throws \RedisException
* @throws \Symfony\Component\Cache\Exception\CacheException
*/
public function getCacheDriver()
public function getCacheAdapter(?string $namespace = null, ?int $defaultLifetime = null): AdapterInterface
{
$setting = $this->driver_setting;
$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 (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
if ($this->config->get('system.cache.cli_compatibility') && (
$setting === 'auto' || $this->isVolatileDriver($setting))) {
if ($this->config->get('system.cache.cli_compatibility') && ($setting === 'auto' || $this->isVolatileDriver($setting))) {
$setting = $driver_name;
}
if (!$setting || $setting === 'auto') {
if ($setting === 'auto' || $this->isVolatileDriver($setting)) {
if (extension_loaded('apcu')) {
$driver_name = 'apcu';
} elseif (extension_loaded('wincache')) {
$driver_name = 'wincache';
}
} else {
$driver_name = $setting;
}
$this->driver_name = $driver_name;
$namespace ??= $this->key;
$defaultLifetime ??= 0;
$resolved_driver_name = $driver_name;
switch ($driver_name) {
case 'apc':
case 'apcu':
$driver = new DoctrineCache\ApcuCache();
break;
case 'wincache':
$driver = new DoctrineCache\WinCacheCache();
break;
case 'memcache':
if (extension_loaded('memcache')) {
$memcache = new \Memcache();
$memcache->connect(
$this->config->get('system.cache.memcache.server', 'localhost'),
$this->config->get('system.cache.memcache.port', 11211)
);
$driver = new DoctrineCache\MemcacheCache();
$driver->setMemcache($memcache);
if (extension_loaded('apcu')) {
$adapter = new ApcuAdapter($namespace, $defaultLifetime);
$resolved_driver_name = 'apcu';
} else {
throw new LogicException('Memcache PHP extension has not been installed');
$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)
);
$driver = new DoctrineCache\MemcachedCache();
$driver->setMemcached($memcached);
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');
}
$driver = new DoctrineCache\RedisCache();
$driver->setRedis($redis);
} 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;
default:
$driver = new DoctrineCache\FilesystemCache($this->cache_dir);
case 'array':
$adapter = new ArrayAdapter($defaultLifetime, false);
$adapter->setNamespace($namespace);
$resolved_driver_name = 'array';
break;
default:
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
* pick the best option based on which cache extensions are installed.
*
* @return CacheProvider The cache driver to use
*/
public function getCacheDriver(?AdapterInterface $adapter = null)
{
if (null === $adapter) {
$adapter = $this->getCacheAdapter();
}
$driver = new SymfonyCacheProvider($adapter);
if ($adapter === $this->adapter) {
$driver->setNamespace($this->key);
}
return $driver;
@@ -479,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;
}
/**
@@ -523,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
@@ -550,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;
}
@@ -669,7 +772,7 @@ class Cache extends Getters
*/
public function isVolatileDriver($setting)
{
return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true);
return in_array($setting, ['apcu', 'array'], true);
}
/**

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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'];
}

View File

@@ -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'
];

View File

@@ -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)) {

View File

@@ -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);

View File

@@ -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;
}
}
@@ -215,7 +215,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
// If strings:
if (is_string($currentVal) && is_string($otherVal)) {
$isValid = (strlen($currentVal) && strlen($otherVal) && (str_contains($currentVal,
$otherVal) || strpos($otherVal, $currentVal) !== false));
$otherVal) || str_contains($otherVal, $currentVal)));
}
// If arrays:
if (is_array($currentVal) && is_array($otherVal)) {

View File

@@ -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;
}
/**

View File

@@ -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;
@@ -323,7 +323,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
* @param FileInterface|null $storage Optionally enter a new storage.
* @return FileInterface|null
*/
public function file(FileInterface $storage = null)
public function file(?FileInterface $storage = null)
{
if ($storage) {
$this->storage = $storage;

View File

@@ -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.
@@ -80,5 +80,5 @@ interface DataInterface
* @param FileInterface|null $storage Optionally enter a new storage.
* @return FileInterface
*/
public function file(FileInterface $storage = null);
public function file(?FileInterface $storage = null);
}

View File

@@ -37,11 +37,10 @@ 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';
@@ -78,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;
}
@@ -87,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) {
@@ -100,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 = [];
@@ -117,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;
@@ -164,7 +162,7 @@ class Validation
* @param UserInterface|null $user
* @return bool
*/
public static function authorize($action, UserInterface $user = null)
public static function authorize($action, ?UserInterface $user = null)
{
if (!$user) {
return false;
@@ -188,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);
@@ -213,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';
}
@@ -228,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;
@@ -241,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) {
@@ -260,7 +257,7 @@ class Validation
return false;
}
if (!$multiline && preg_match('/\R/um', $value)) {
if (!$multiline && preg_match('/\R/um', (string) $value)) {
return false;
}
@@ -268,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 '';
@@ -289,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');
@@ -303,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;
@@ -329,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);
}
@@ -368,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;
@@ -385,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;
@@ -402,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);
}
@@ -415,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;
@@ -424,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);
}
@@ -442,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');
@@ -458,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);
}
@@ -471,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;
@@ -488,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;
}
@@ -512,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);
}
@@ -525,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;
@@ -560,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) {
@@ -594,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);
}
@@ -618,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);
}
/**
@@ -631,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;
@@ -641,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;
}
}
@@ -660,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;
@@ -677,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;
@@ -702,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);
}
@@ -715,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';
@@ -732,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';
@@ -749,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';
@@ -766,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;
}
@@ -783,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;
@@ -831,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);
@@ -844,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']) : [];
@@ -873,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);
}
}
}
@@ -897,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]);
}
@@ -939,7 +917,7 @@ class Validation
$val = (string)$val;
break;
case 'trim':
$val = trim($val);
$val = trim((string) $val);
break;
}
}
@@ -954,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;
@@ -968,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);
}
@@ -979,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;
@@ -1011,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;
}
@@ -1035,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 !== '';
@@ -1068,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);
}
/**
@@ -1177,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));
@@ -1185,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));
}
}

View File

@@ -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;
}
/**

View File

@@ -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));
}
@@ -544,7 +542,7 @@ class Debugger
* @param string|null $message
* @return mixed
*/
public function profile(callable $callable, string $message = null)
public function profile(callable $callable, ?string $message = null)
{
$this->startProfiling();
$response = $callable();
@@ -585,7 +583,7 @@ class Debugger
* @param string|null $message
* @return array|null
*/
public function stopProfiling(string $message = null): ?array
public function stopProfiling(?string $message = null): ?array
{
$timings = null;
if ($this->enabled && extension_loaded('tideways_xhprof')) {
@@ -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) {
@@ -776,10 +769,10 @@ class Debugger
* @param float|null $time
* @return $this
*/
public function addEvent(string $name, $event, EventDispatcherInterface $dispatcher, float $time = null)
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';

View File

@@ -70,7 +70,7 @@ class Errors
$logger = $grav['log'];
$whoops->pushHandler(function ($exception, $inspector, $run) use ($logger) {
try {
$logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
$logger->critical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
} catch (Exception $e) {
echo $e;
}

View File

@@ -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);

View File

@@ -28,28 +28,27 @@ trait CompiledFile
/**
* Get/set parsed file contents.
*
* @param mixed $var
* @return array
*/
public function content($var = null)
public function content(mixed $var = null)
{
try {
$filename = $this->filename;
// If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($filename);
$key = md5((string) $filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified();
if (!$modified) {
try {
return $this->decode($this->raw());
} catch (Throwable $e) {
} catch (Throwable) {
// If the compiled file is broken, we can safely ignore the error and continue.
}
}
$class = get_class($this);
$class = $this::class;
$size = filesize($filename);
$cache = $file->exists() ? $file->content() : null;
@@ -115,7 +114,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);
@@ -135,7 +134,7 @@ trait CompiledFile
if ($locked) {
$modified = $this->modified();
$filename = $this->filename;
$class = get_class($this);
$class = $this::class;
$size = filesize($filename);
// windows doesn't play nicely with this as it can't read when locked

View File

@@ -75,21 +75,21 @@ abstract class Archiver
* @param callable|null $status
* @return $this
*/
abstract public function compress($folder, callable $status = null);
abstract public function compress($folder, ?callable $status = null);
/**
* @param string $destination
* @param callable|null $status
* @return $this
*/
abstract public function extract($destination, callable $status = null);
abstract public function extract($destination, ?callable $status = null);
/**
* @param array $folders
* @param callable|null $status
* @return $this
*/
abstract public function addEmptyFolders($folders, callable $status = null);
abstract public function addEmptyFolders($folders, ?callable $status = null);
/**
* @param string $rootPath

View File

@@ -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');
}

View File

@@ -63,8 +63,8 @@ class RecursiveDirectoryFilterIterator extends RecursiveFilterIterator
}
// Check if any parent directory is in the ignore list
foreach ($this::$ignore_folders as $ignore_folder) {
$ignore_folder = trim($ignore_folder, '/');
if (strpos($relative_filename, $ignore_folder . '/') === 0 || $relative_filename === $ignore_folder) {
$ignore_folder = trim((string) $ignore_folder, '/');
if (str_starts_with($relative_filename, $ignore_folder . '/') || $relative_filename === $ignore_folder) {
return false;
}
}
@@ -92,12 +92,12 @@ class RecursiveDirectoryFilterIterator extends RecursiveFilterIterator
return true;
}
// Check for extension patterns like .pdf
if (strpos($pattern, '.') === 0 && substr($filename, -strlen($pattern)) === $pattern) {
if (str_starts_with((string) $pattern, '.') && str_ends_with($filename, (string) $pattern)) {
return true;
}
// Check for wildcard patterns
if (strpos($pattern, '*') !== false) {
$regex = '/^' . str_replace('\\*', '.*', preg_quote($pattern, '/')) . '$/';
if (str_contains((string) $pattern, '*')) {
$regex = '/^' . str_replace('\\*', '.*', preg_quote((string) $pattern, '/')) . '$/';
if (preg_match($regex, $filename)) {
return true;
}

View File

@@ -26,7 +26,7 @@ class ZipArchiver extends Archiver
* @param callable|null $status
* @return $this
*/
public function extract($destination, callable $status = null)
public function extract($destination, ?callable $status = null)
{
$zip = new ZipArchive();
$archive = $zip->open($this->archive_file);
@@ -51,7 +51,7 @@ class ZipArchiver extends Archiver
* @param callable|null $status
* @return $this
*/
public function compress($source, callable $status = null)
public function compress($source, ?callable $status = null)
{
if (!extension_loaded('zip')) {
throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
@@ -90,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);
@@ -118,7 +118,7 @@ class ZipArchiver extends Archiver
* @param callable|null $status
* @return $this
*/
public function addEmptyFolders($folders, callable $status = null)
public function addEmptyFolders($folders, ?callable $status = null)
{
if (!extension_loaded('zip')) {
throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');

View File

@@ -32,7 +32,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements Med
* {@inheritdoc}
* @see FlexObjectInterface::getFormValue()
*/
public function getFormValue(string $name, $default = null, string $separator = null)
public function getFormValue(string $name, $default = null, ?string $separator = null)
{
$value = $this->getNestedProperty($name, null, $separator);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -172,7 +172,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @return static
* @phpstan-return static<T>
*/
public function merge(PageCollectionInterface $collection)
public function merge(PageCollectionInterface $collection): never
{
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
@@ -184,7 +184,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @return static
* @phpstan-return static<T>
*/
public function intersect(PageCollectionInterface $collection)
public function intersect(PageCollectionInterface $collection): never
{
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
@@ -242,7 +242,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @return static
* @phpstan-return static<T>
*/
public function append($items)
public function append($items): never
{
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
@@ -303,7 +303,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
// do this header query work only once
$header_query = null;
$header_default = null;
if (strpos($order_by, 'header.') === 0) {
if (str_starts_with($order_by, 'header.')) {
$query = explode('|', str_replace('header.', '', $order_by), 2);
$header_query = array_shift($query) ?? '';
$header_default = array_shift($query);
@@ -373,9 +373,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
if ($col) {
$col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
$list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
return sprintf('%032d.', $number[0]);
}, $list);
$list = preg_replace_callback('~([0-9]+)\.~', static fn($number) => sprintf('%032d.', $number[0]), $list);
if (!is_array($list)) {
throw new RuntimeException('Internal Error');
}
@@ -457,7 +455,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
continue;
}
$date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
$date = $field ? strtotime((string) $object->getNestedProperty($field)) : $object->date();
if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
$entries[$key] = $object;
@@ -766,7 +764,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @return static
* @phpstan-return static<T>
*/
public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
public function withTranslation(bool $bool = true, ?string $languageCode = null, ?bool $fallback = null)
{
$list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback])));
@@ -778,7 +776,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @param bool|null $fallback
* @return PageIndex
*/
public function withTranslated(string $languageCode = null, bool $fallback = null)
public function withTranslated(?string $languageCode = null, ?bool $fallback = null)
{
return $this->getIndex()->withTranslated($languageCode, $fallback);
}

View File

@@ -57,7 +57,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
public const ORDER_LIST_REGEX = '/(\/\d+)\.[^\/]+/u';
public const PAGE_ROUTE_REGEX = '/\/\d+\./u';
/** @var PageObject|array */
/** @var T|array */
protected $_root;
/** @var array|null */
protected $_params;
@@ -66,7 +66,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param array $entries
* @param FlexDirectory|null $directory
*/
public function __construct(array $entries = [], FlexDirectory $directory = null)
public function __construct(array $entries = [], ?FlexDirectory $directory = null)
{
// Remove root if it's taken.
if (isset($entries[''])) {
@@ -181,7 +181,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @return static
* @phpstan-return static<T,C>
*/
public function withTranslated(string $languageCode = null, bool $fallback = null)
public function withTranslated(?string $languageCode = null, ?bool $fallback = null)
{
if (null === $languageCode) {
return $this;
@@ -239,10 +239,9 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Set a parameter to the Collection
*
* @param string $name
* @param mixed $value
* @return $this
*/
public function setParam(string $name, $value)
public function setParam(string $name, mixed $value)
{
$this->_params[$name] = $value;
@@ -413,7 +412,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @return static
* @phpstan-return static<T,C>
*/
protected function createFrom(array $entries, string $keyField = null)
protected function createFrom(array $entries, ?string $keyField = null)
{
/** @var static $index */
$index = parent::createFrom($entries, $keyField);
@@ -428,7 +427,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param bool|null $fallback
* @return array
*/
protected function translateEntries(array $entries, string $lang, bool $fallback = null): array
protected function translateEntries(array $entries, string $lang, ?bool $fallback = null): array
{
$languages = $this->getFallbackLanguages($lang, $fallback);
foreach ($entries as $key => &$entry) {
@@ -493,9 +492,9 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param bool|null $fallback
* @return array
*/
protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
protected function getFallbackLanguages(?string $languageCode = null, ?bool $fallback = null): array
{
$fallback = $fallback ?? true;
$fallback ??= true;
if (!$fallback && null !== $languageCode) {
return [$languageCode];
}
@@ -504,7 +503,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/** @var Language $language */
$language = $grav['language'];
$languageCode = $languageCode ?? '';
$languageCode ??= '';
if ($languageCode === '' && $fallback) {
return $language->getFallbackLanguages(null, true);
}
@@ -533,7 +532,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
// Handle leaf_route
$leaf = null;
if ($leaf_route && $route !== $leaf_route) {
$nodes = explode('/', $leaf_route);
$nodes = explode('/', (string) $leaf_route);
$sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++));
$options['route'] = $sub_route;
@@ -544,7 +543,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
if (!$route) {
$page = $this->getRoot();
} else {
$page = $this->get(trim($route, '/'));
$page = $this->get(trim((string) $route, '/'));
}
$path = $page ? $page->path() : null;
@@ -558,7 +557,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
// Clean up filter.
$filter_type = (array)($filters['type'] ?? []);
unset($filters['type']);
$filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; });
$filters = array_filter($filters, static fn($val) => $val !== null && $val !== '');
if ($page) {
$status = 'success';
@@ -682,9 +681,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
'tags' => $tags,
'actions' => $this->getListingActions($child, $user),
];
$extras = array_filter($extras, static function ($v) {
return $v !== null;
});
$extras = array_filter($extras, static fn($v) => $v !== null);
/** @var PageIndex $tmp */
$tmp = $child->children()->getIndex();
@@ -698,7 +695,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
'title' => htmlspecialchars($child->menu()),
'route' => [
'display' => htmlspecialchars($route) ?: null,
'raw' => htmlspecialchars($child->rawRoute()),
'raw' => htmlspecialchars((string) $child->rawRoute()),
],
'modified' => $this->jsDate($child->modified()),
'child_count' => $child_count ?: null,
@@ -706,9 +703,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null,
'extras' => $extras
];
$payload = array_filter($payload, static function ($v) {
return $v !== null;
});
$payload = array_filter($payload, static fn($v) => $v !== null);
}
// Add children if any
@@ -781,7 +776,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param int|null $timestamp
* @return string|null
*/
private function jsDate(int $timestamp = null): ?string
private function jsDate(?int $timestamp = null): ?string
{
if (!$timestamp) {
return null;

View File

@@ -129,7 +129,7 @@ class PageObject extends FlexPageObject
/**
* @inheritdoc PageInterface
*/
public function getFormValue(string $name, $default = null, string $separator = null)
public function getFormValue(string $name, $default = null, ?string $separator = null)
{
$test = new stdClass();
@@ -261,7 +261,7 @@ class PageObject extends FlexPageObject
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
public function check(?UserInterface $user = null): void
{
parent::check($user);
@@ -521,7 +521,7 @@ class PageObject extends FlexPageObject
$template = $this->getProperty('template') . ($name ? '.' . $name : '');
$blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
} catch (RuntimeException $e) {
} catch (RuntimeException) {
$template = 'default' . ($name ? '.' . $name : '');
$blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
@@ -554,7 +554,7 @@ class PageObject extends FlexPageObject
$initial = $options['initial'] ?? null;
$var = $initial ? 'leaf_route' : 'route';
$route = $options[$var] ?? '';
if ($route !== '' && !str_starts_with($route, '/')) {
if ($route !== '' && !str_starts_with((string) $route, '/')) {
$filesystem = Filesystem::getInstance();
$route = "/{$this->getKey()}/{$route}";
@@ -600,7 +600,7 @@ class PageObject extends FlexPageObject
$matches = $test->search((string)$value) > 0.0;
break;
case 'page_type':
$types = $value ? explode(',', $value) : [];
$types = $value ? explode(',', (string) $value) : [];
$matches = in_array($test->template(), $types, true);
break;
case 'extension':
@@ -698,7 +698,7 @@ class PageObject extends FlexPageObject
} elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
// Store ordering.
$ordering = $elements['order'] ?? null;
$this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
$this->_reorder = !empty($ordering) ? explode(',', (string) $ordering) : [];
$order = false;
if ((bool)($elements['ordering'] ?? false)) {

View File

@@ -394,14 +394,14 @@ class PageStorage extends FolderStorage
if ($oldFolder !== $newFolder && file_exists($oldFolder)) {
$isCopy = $row['__META']['copy'] ?? false;
if ($isCopy) {
if (strpos($newFolder, $oldFolder . '/') === 0) {
if (str_starts_with($newFolder, $oldFolder . '/')) {
throw new RuntimeException(sprintf('Page /%s cannot be copied to itself', $oldKey));
}
$this->copyRow($oldKey, $newKey);
$debugger->addMessage("Page copied: {$oldFolder} => {$newFolder}", 'debug');
} else {
if (strpos($newFolder, $oldFolder . '/') === 0) {
if (str_starts_with($newFolder, $oldFolder . '/')) {
throw new RuntimeException(sprintf('Page /%s cannot be moved to itself', $oldKey));
}
@@ -538,7 +538,7 @@ class PageStorage extends FolderStorage
if ($reload || !isset($this->meta[$key])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if (mb_strpos($key, '@@') === false) {
if (mb_strpos((string) $key, '@@') === false) {
$path = $this->getStoragePath($key);
if (is_string($path)) {
$path = $locator->isStream($path) ? $locator->findResource($path) : GRAV_ROOT . "/{$path}";

View File

@@ -32,7 +32,7 @@ trait PageRoutableTrait
* @return PageInterface|null the parent page object if it exists.
*/
public function parent(PageInterface $var = null)
public function parent(?PageInterface $var = null)
{
if (Utils::isAdminPlugin()) {
return parent::parent();

View File

@@ -92,9 +92,7 @@ trait PageTranslateTrait
$list[$languageCode ?: $defaultCode] = $route ?? '';
}
$list = array_filter($list, static function ($var) {
return null !== $var;
});
$list = array_filter($list, static fn($var) => null !== $var);
// Hack to get the same result as with old pages.
foreach ($list as &$path) {

View File

@@ -38,7 +38,7 @@ class UserGroupCollection extends FlexCollection
* @param string|null $scope
* @return bool|null
*/
public function authorize(string $action, string $scope = null): ?bool
public function authorize(string $action, ?string $scope = null): ?bool
{
$authorized = null;
/** @var UserGroupObject $object */

View File

@@ -57,7 +57,7 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
* @param string|null $scope
* @return bool|null
*/
public function authorize(string $action, string $scope = null): ?bool
public function authorize(string $action, ?string $scope = null): ?bool
{
if ($scope === 'test') {
$scope = null;
@@ -100,10 +100,9 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
}
/**
* @param mixed $value
* @return array
*/
protected function offsetLoad_access($value): array
protected function offsetLoad_access(mixed $value): array
{
if (!$value instanceof Access) {
$value = new Access($value);
@@ -115,10 +114,9 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
}
/**
* @param mixed $value
* @return array
*/
protected function offsetPrepare_access($value): array
protected function offsetPrepare_access(mixed $value): array
{
return $this->offsetLoad_access($value);
}

View File

@@ -23,7 +23,7 @@ class UserFileStorage extends FileStorage
* {@inheritdoc}
* @see FlexStorageInterface::getMediaPath()
*/
public function getMediaPath(string $key = null): ?string
public function getMediaPath(?string $key = null): ?string
{
// There is no media support for file storage (fallback to common location).
return null;

View File

@@ -30,7 +30,7 @@ trait UserObjectLegacyTrait
*/
public function merge(array $data)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED);
$this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data));
@@ -45,7 +45,7 @@ trait UserObjectLegacyTrait
*/
public function getAvatarMedia()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED);
return $this->getAvatarImage();
}
@@ -58,7 +58,7 @@ trait UserObjectLegacyTrait
*/
public function avatarUrl()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED);
return $this->getAvatarUrl();
}
@@ -73,7 +73,7 @@ trait UserObjectLegacyTrait
*/
public function authorise($action)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED);
return $this->authorize($action) ?? false;
}
@@ -87,7 +87,7 @@ trait UserObjectLegacyTrait
#[\ReturnTypeWillChange]
public function count()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
return count($this->jsonSerialize());
}

View File

@@ -67,7 +67,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
// Username can also be number and stored as such.
$key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
$meta['key'] = static::filterUsername($key, $storage);
$meta['email'] = isset($data['email']) ? mb_strtolower($data['email']) : null;
$meta['email'] = isset($data['email']) ? mb_strtolower((string) $data['email']) : null;
}
/**
@@ -197,7 +197,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
/** @var Logger $logger */
$logger = $grav['log'];
$logger->addDebug($message);
$logger->debug($message);
/** @var Debugger $debugger */
$debugger = $grav['debugger'];

View File

@@ -92,9 +92,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
protected $_uploads_original;
/** @var FileInterface|null */
protected $_storage;
/** @var UserGroupIndex */
/** @var UserGroupIndex|null */
protected $_groups;
/** @var Access */
/** @var Access|null */
protected $_access;
/** @var array|null */
protected $access;
@@ -270,7 +270,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
* @param string|null $scope
* @return bool|null
*/
public function authorize(string $action, string $scope = null): ?bool
public function authorize(string $action, ?string $scope = null): ?bool
{
if ($scope === 'test') {
// Special scope to test user permissions.
@@ -286,7 +286,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
return false;
}
if (strpos($action, 'login') === false && !$this->getProperty('authorized')) {
if (!str_contains($action, 'login') && !$this->getProperty('authorized')) {
// User needs to be authorized (2FA).
return false;
}
@@ -401,7 +401,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
*/
public function join($name, $value, $separator = null)
{
$separator = $separator ?? '.';
$separator ??= '.';
$old = $this->get($name, null, $separator);
if ($old !== null) {
if (!is_array($old)) {
@@ -557,7 +557,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
* @param FileInterface|null $storage Optionally enter a new storage.
* @return FileInterface|null
*/
public function file(FileInterface $storage = null)
public function file(?FileInterface $storage = null)
{
if (null !== $storage) {
$this->_storage = $storage;
@@ -1027,10 +1027,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
/**
* @param mixed $value
* @return array
*/
protected function offsetLoad_access($value): array
protected function offsetLoad_access(mixed $value): array
{
if (!$value instanceof Access) {
$value = new Access($value);
@@ -1040,10 +1039,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
/**
* @param mixed $value
* @return array
*/
protected function offsetPrepare_access($value): array
protected function offsetPrepare_access(mixed $value): array
{
return $this->offsetLoad_access($value);
}

View File

@@ -28,7 +28,7 @@ class FormFlash extends FrameworkFormFlash
{
$fields = [];
foreach ($this->files as $field => $files) {
if (strpos($field, '/')) {
if (strpos((string) $field, '/')) {
continue;
}
foreach ($files as $file) {

View File

@@ -14,7 +14,7 @@ use Grav\Common\Data\Data;
/**
* @property string $name
*/
class Package
class Package implements \Stringable
{
/** @var Data */
protected $data;
@@ -53,11 +53,10 @@ class Package
/**
* @param string $key
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function __set($key, $value)
public function __set($key, mixed $value)
{
$this->data->set($key, $value);
}
@@ -76,7 +75,7 @@ class Package
* @return string
*/
#[\ReturnTypeWillChange]
public function __toString()
public function __toString(): string
{
return $this->toJson();
}

View File

@@ -10,6 +10,7 @@
namespace Grav\Common\GPM;
use Exception;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder;
use Grav\Common\HTTP\Response;
@@ -24,6 +25,7 @@ use function count;
use function in_array;
use function is_array;
use function is_object;
use function property_exists;
/**
* Class GPM
@@ -37,8 +39,6 @@ class GPM extends Iterator
private $repository;
/** @var Remote\GravCore|null Remove Grav Packages */
private $grav;
/** @var bool */
private $refresh;
/** @var callable|null */
private $callback;
@@ -57,7 +57,7 @@ class GPM extends Iterator
* @param bool $refresh Applies to Remote Packages only and forces a refetch of data
* @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
public function __construct(private $refresh = false, $callback = null)
{
parent::__construct();
@@ -65,7 +65,6 @@ class GPM extends Iterator
$this->cache = [];
$this->installed = new Local\Packages();
$this->refresh = $refresh;
$this->callback = $callback;
}
@@ -78,12 +77,10 @@ class GPM extends Iterator
#[\ReturnTypeWillChange]
public function __get($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav();
}
return parent::__get($offset);
return match ($offset) {
'grav' => $this->getGrav(),
default => parent::__get($offset),
};
}
/**
@@ -95,12 +92,10 @@ class GPM extends Iterator
#[\ReturnTypeWillChange]
public function __isset($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav() !== null;
}
return parent::__isset($offset);
return match ($offset) {
'grav' => $this->getGrav() !== null,
default => parent::__isset($offset),
};
}
/**
@@ -126,7 +121,7 @@ class GPM extends Iterator
if ($type_installed === false) {
continue;
}
$methodInstallableType = 'getInstalled' . ucfirst($type);
$methodInstallableType = 'getInstalled' . ucfirst((string) $type);
$to_install = $this->$methodInstallableType();
$items[$type] = $to_install;
$items['total'] += count($to_install);
@@ -286,7 +281,7 @@ class GPM extends Iterator
if ($type_updatable === false) {
continue;
}
$methodUpdatableType = 'getUpdatable' . ucfirst($type);
$methodUpdatableType = 'getUpdatable' . ucfirst((string) $type);
$to_update = $this->$methodUpdatableType();
$items[$type] = $to_update;
$items['total'] += count($to_update);
@@ -322,6 +317,10 @@ class GPM extends Iterator
continue;
}
if (!$this->isRemotePackagePublished($plugins[$slug])) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $plugins[$slug]->version;
@@ -414,6 +413,10 @@ class GPM extends Iterator
continue;
}
if (!$this->isRemotePackagePublished($themes[$slug])) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $themes[$slug]->version;
@@ -468,6 +471,42 @@ class GPM extends Iterator
return null;
}
/**
* Determine whether a remote package is marked as published.
*
* Remote package metadata introduced a `published` flag to hide releases that are not yet public.
* Older repository payloads may omit the key, so we default to treating packages as published
* unless the flag is explicitly set to `false`.
*
* @param object|array $package
* @return bool
*/
protected function isRemotePackagePublished($package): bool
{
if (is_object($package) && method_exists($package, 'getData')) {
$data = $package->getData();
if ($data instanceof Data) {
$published = $data->get('published');
return $published !== false;
}
}
if (is_array($package)) {
if (array_key_exists('published', $package)) {
return $package['published'] !== false;
}
return true;
}
$value = null;
if (is_object($package) && property_exists($package, 'published')) {
$value = $package->published;
}
return $value !== false;
}
/**
* Returns true if the package latest release is stable
*
@@ -550,7 +589,7 @@ class GPM extends Iterator
if (null === $this->repository) {
try {
$this->repository = new Remote\Packages($this->refresh, $this->callback);
} catch (Exception $e) {}
} catch (Exception) {}
}
return $this->repository;
@@ -566,7 +605,7 @@ class GPM extends Iterator
if (null === $this->grav) {
try {
$this->grav = new Remote\GravCore($this->refresh, $this->callback);
} catch (Exception $e) {}
} catch (Exception) {}
}
return $this->grav;
@@ -1220,7 +1259,7 @@ class GPM extends Iterator
*/
public function versionFormatIsNextSignificantRelease($version): bool
{
return strpos($version, '~') === 0;
return str_starts_with($version, '~');
}
/**
@@ -1233,7 +1272,7 @@ class GPM extends Iterator
*/
public function versionFormatIsEqualOrHigher($version): bool
{
return strpos($version, '>=') === 0;
return str_starts_with($version, '>=');
}
/**

View File

@@ -81,7 +81,7 @@ class Installer
{
$destination = rtrim($destination, DS);
$options = array_merge(self::$options, $options);
$install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS);
$install_path = rtrim($destination . DS . ltrim((string) $options['install_path'], DS), DS);
if (!self::isGravInstance($destination) || !self::isValidDestination(
$install_path,

View File

@@ -37,7 +37,7 @@ class Package extends BasePackage
$html_description = Parsedown::instance()->line($this->__get('description'));
$this->data->set('slug', $package->__get('slug'));
$this->data->set('description_html', $html_description);
$this->data->set('description_plain', strip_tags($html_description));
$this->data->set('description_plain', strip_tags((string) $html_description));
$this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->__get('slug')));
}

View File

@@ -12,7 +12,7 @@ namespace Grav\Common\GPM\Remote;
use Grav\Common\Grav;
use Grav\Common\HTTP\Response;
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
use \Doctrine\Common\Cache\FilesystemCache;
use Doctrine\Common\Cache\FilesystemCache;
use RuntimeException;
/**
@@ -53,10 +53,10 @@ class AbstractPackageCollection extends BaseCollection
$this->raw = $this->cache->fetch(md5($this->repository));
$this->fetch($refresh, $callback);
foreach (json_decode($this->raw, true) as $slug => $data) {
foreach (json_decode((string) $this->raw, true) as $slug => $data) {
// Temporarily fix for using multi-sites
if (isset($data['install_path'])) {
$path = preg_replace('~^user/~i', 'user://', $data['install_path']);
$path = preg_replace('~^user/~i', 'user://', (string) $data['install_path']);
$data['install_path'] = Grav::instance()['locator']->findResource($path, false, true);
}
$this->items[$slug] = new Package($data, $this->type);

View File

@@ -21,7 +21,6 @@ class GravCore extends AbstractPackageCollection
{
/** @var string */
protected $repository = 'https://getgrav.org/downloads/grav.json';
/** @var array */
private $data;
/** @var string */
@@ -46,7 +45,7 @@ class GravCore extends AbstractPackageCollection
$this->fetch($refresh, $callback);
$this->data = json_decode($this->raw, true);
$this->data = json_decode((string) $this->raw, true);
$this->version = $this->data['version'] ?? '-';
$this->date = $this->data['date'] ?? '-';
$this->min_php = $this->data['min_php'] ?? null;
@@ -82,7 +81,7 @@ class GravCore extends AbstractPackageCollection
$diffLog = [];
foreach ((array)$this->data['changelog'] as $version => $changelog) {
preg_match("/[\w\-\.]+/", $version, $cleanVersion);
preg_match("/[\w\-\.]+/", (string) $version, $cleanVersion);
if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) {
continue;

View File

@@ -52,7 +52,7 @@ class Package extends BasePackage implements \JsonSerializable
$diffLog = [];
foreach ((array)$this->data['changelog'] as $version => $changelog) {
preg_match("/[\w\-.]+/", $version, $cleanVersion);
preg_match("/[\w\-.]+/", (string) $version, $cleanVersion);
if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) {
continue;

View File

@@ -29,7 +29,7 @@ abstract class Getters implements ArrayAccess, Countable
* @param mixed $value Medium value
*/
#[\ReturnTypeWillChange]
public function __set($offset, $value)
public function __set($offset, mixed $value)
{
$this->offsetSet($offset, $value);
}
@@ -103,10 +103,9 @@ abstract class Getters implements ArrayAccess, Countable
/**
* @param int|string $offset
* @param mixed $value
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
public function offsetSet($offset, mixed $value)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;

View File

@@ -202,7 +202,7 @@ class Grav extends Container
* @param string|null $environment
* @return $this
*/
public function setup(string $environment = null)
public function setup(?string $environment = null)
{
if (isset($this->initialized['setup'])) {
return $this;
@@ -263,51 +263,23 @@ class Grav extends Container
$container = new Container(
[
'multipartRequestSupport' => function () {
return new MultipartRequestSupport();
},
'initializeProcessor' => function () {
return new InitializeProcessor($this);
},
'backupsProcessor' => function () {
return new BackupsProcessor($this);
},
'pluginsProcessor' => function () {
return new PluginsProcessor($this);
},
'themesProcessor' => function () {
return new ThemesProcessor($this);
},
'schedulerProcessor' => function () {
return new SchedulerProcessor($this);
},
'requestProcessor' => function () {
return new RequestProcessor($this);
},
'tasksProcessor' => function () {
return new TasksProcessor($this);
},
'assetsProcessor' => function () {
return new AssetsProcessor($this);
},
'twigProcessor' => function () {
return new TwigProcessor($this);
},
'pagesProcessor' => function () {
return new PagesProcessor($this);
},
'debuggerAssetsProcessor' => function () {
return new DebuggerAssetsProcessor($this);
},
'renderProcessor' => function () {
return new RenderProcessor($this);
},
'multipartRequestSupport' => fn() => new MultipartRequestSupport(),
'initializeProcessor' => fn() => new InitializeProcessor($this),
'backupsProcessor' => fn() => new BackupsProcessor($this),
'pluginsProcessor' => fn() => new PluginsProcessor($this),
'themesProcessor' => fn() => new ThemesProcessor($this),
'schedulerProcessor' => fn() => new SchedulerProcessor($this),
'requestProcessor' => fn() => new RequestProcessor($this),
'tasksProcessor' => fn() => new TasksProcessor($this),
'assetsProcessor' => fn() => new AssetsProcessor($this),
'twigProcessor' => fn() => new TwigProcessor($this),
'pagesProcessor' => fn() => new PagesProcessor($this),
'debuggerAssetsProcessor' => fn() => new DebuggerAssetsProcessor($this),
'renderProcessor' => fn() => new RenderProcessor($this),
]
);
$default = static function () {
return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
};
$default = static fn() => new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
$collection = new RequestHandler($this->middleware, $default, $container);
@@ -328,7 +300,7 @@ class Grav extends Container
$etag = md5($body);
$response = $response->withHeader('ETag', '"' . $etag . '"');
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
$search = trim((string) $this['request']->getHeaderLine('If-None-Match'), '"');
if ($noCache === false && $search === $etag) {
$response = $response->withStatus(304);
$body = '';
@@ -405,7 +377,7 @@ class Grav extends Container
$etag = md5($body);
$response = $response->withHeader('ETag', '"' . $etag . '"');
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
$search = trim((string) $this['request']->getHeaderLine('If-None-Match'), '"');
if ($noCache === false && $search === $etag) {
$response = $response->withStatus(304);
$body = '';
@@ -463,7 +435,7 @@ class Grav extends Container
if (null === $code) {
// Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html
$regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/';
preg_match($regex, $route, $matches);
preg_match($regex, (string) $route, $matches);
if ($matches) {
$route = str_replace($matches[1], '', $matches[0]);
$code = $matches[2];
@@ -476,9 +448,9 @@ class Grav extends Container
$url = rtrim($uri->rootUrl(), '/') . '/';
if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
$url .= trim($route, '/'); // Remove trailing slash
$url .= trim((string) $route, '/'); // Remove trailing slash
} else {
$url .= ltrim($route, '/'); // Support trailing slash default routes
$url .= ltrim((string) $route, '/'); // Support trailing slash default routes
}
}
} elseif ($route instanceof Route) {
@@ -524,7 +496,7 @@ class Grav extends Container
* @param ResponseInterface|null $response
* @return void
*/
public function header(ResponseInterface $response = null): void
public function header(?ResponseInterface $response = null): void
{
if (null === $response) {
/** @var PageInterface $page */
@@ -535,7 +507,7 @@ class Grav extends Container
header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
foreach ($response->getHeaders() as $key => $values) {
// Skip internal Grav headers.
if (strpos($key, 'Grav-Internal-') === 0) {
if (str_starts_with((string) $key, 'Grav-Internal-')) {
continue;
}
foreach ($values as $i => $value) {
@@ -554,7 +526,7 @@ class Grav extends Container
// Initialize Locale if set and configured.
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
$language = $this['language']->getLanguage();
setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
setlocale(LC_ALL, strlen((string) $language) < 3 ? ($language . '_' . strtoupper((string) $language)) : $language);
} elseif ($this['config']->get('system.default_locale')) {
setlocale(LC_ALL, $this['config']->get('system.default_locale'));
}
@@ -568,7 +540,7 @@ class Grav extends Container
{
/** @var EventDispatcherInterface $events */
$events = $this['events'];
$eventName = get_class($event);
$eventName = $event::class;
$timestamp = microtime(true);
$event = $events->dispatch($event);
@@ -587,7 +559,7 @@ class Grav extends Container
* @param Event|null $event
* @return Event
*/
public function fireEvent($eventName, Event $event = null)
public function fireEvent($eventName, ?Event $event = null)
{
/** @var EventDispatcherInterface $events */
$events = $this['events'];
@@ -600,7 +572,9 @@ class Grav extends Container
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$debugger->addEvent($eventName, $event, $events, $timestamp);
if ($debugger->enabled()) {
$debugger->addEvent($eventName, $event, $events, $timestamp);
}
return $event;
}
@@ -736,9 +710,7 @@ class Grav extends Container
if (is_int($serviceKey)) {
$this->register(new $serviceClass);
} else {
$this[$serviceKey] = function ($c) use ($serviceClass) {
return new $serviceClass($c);
};
$this[$serviceKey] = fn($c) => new $serviceClass($c);
}
}
}
@@ -801,7 +773,7 @@ class Grav extends Container
$medium = $media[$media_file];
foreach ($uri->query(null, true) as $action => $params) {
if (in_array($action, ImageMedium::$magic_actions, true)) {
call_user_func_array([&$medium, $action], explode(',', $params));
call_user_func_array([&$medium, $action], explode(',', (string) $params));
}
}
Utils::download($medium->path(), false);
@@ -818,7 +790,7 @@ class Grav extends Container
if ($extension) {
$download = true;
if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
if (in_array(ltrim((string) $extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
$download = false;
}
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);

View File

@@ -25,7 +25,7 @@ class Client
'User-Agent' => 'Grav CMS'
];
public static function getClient(array $overrides = [], int $connections = 6, callable $callback = null): HttpClientInterface
public static function getClient(array $overrides = [], int $connections = 6, ?callable $callback = null): HttpClientInterface
{
$config = Grav::instance()['config'];
$options = static::getOptions();
@@ -33,7 +33,7 @@ class Client
// Use callback if provided
if ($callback) {
self::$callback = $callback;
$options->setOnProgress([Client::class, 'progress']);
$options->setOnProgress(Client::progress(...));
}
$settings = array_merge($options->toArray(), $overrides);
@@ -43,17 +43,11 @@ class Client
$preferred_method = $config->get('system.gpm.method', 'auto');
}
switch ($preferred_method) {
case 'curl':
$client = new CurlHttpClient($settings, $connections);
break;
case 'fopen':
case 'native':
$client = new NativeHttpClient($settings, $connections);
break;
default:
$client = HttpClient::create($settings, $connections);
}
$client = match ($preferred_method) {
'curl' => new CurlHttpClient($settings, $connections),
'fopen', 'native' => new NativeHttpClient($settings, $connections),
default => HttpClient::create($settings, $connections),
};
return $client;
}

View File

@@ -41,7 +41,7 @@ class Response
* @return string
* @throws TransportExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface|ClientExceptionInterface
*/
public static function get(string $uri = '', array $overrides = [], callable $callback = null): string
public static function get(string $uri = '', array $overrides = [], ?callable $callback = null): string
{
$response = static::request('GET', $uri, $overrides, $callback);
return $response->getContent();
@@ -58,7 +58,7 @@ class Response
* @return ResponseInterface
* @throws TransportExceptionInterface
*/
public static function request(string $method, string $uri, array $overrides = [], callable $callback = null): ResponseInterface
public static function request(string $method, string $uri, array $overrides = [], ?callable $callback = null): ResponseInterface
{
if (empty($method)) {
throw new TransportException('missing method (GET, PUT, etc.)');
@@ -73,7 +73,7 @@ class Response
if (Utils::functionExists('set_time_limit')) {
@set_time_limit(0);
}
} catch (Exception $e) {}
} catch (Exception) {}
$client = Client::getClient($overrides, 6, $callback);

View File

@@ -30,7 +30,7 @@ class Excerpts
* @param PageInterface|null $page Page, defaults to the current page object
* @return string Returns final HTML string
*/
public static function processImageHtml($html, PageInterface $page = null)
public static function processImageHtml($html, ?PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
if (null === $excerpt) {
@@ -61,7 +61,7 @@ class Excerpts
* @param PageInterface|null $page Page, defaults to the current page object
* @return string Returns final HTML string
*/
public static function processLinkHtml($html, PageInterface $page = null)
public static function processLinkHtml($html, ?PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'a');
if (null === $excerpt) {
@@ -158,7 +158,7 @@ class Excerpts
* @param string $type
* @return mixed
*/
public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link')
public static function processLinkExcerpt($excerpt, ?PageInterface $page = null, $type = 'link')
{
$excerpts = new ExcerptsObject($page);
@@ -172,7 +172,7 @@ class Excerpts
* @param PageInterface|null $page Page, defaults to the current page object
* @return array
*/
public static function processImageExcerpt(array $excerpt, PageInterface $page = null)
public static function processImageExcerpt(array $excerpt, ?PageInterface $page = null)
{
$excerpts = new ExcerptsObject($page);
@@ -187,7 +187,7 @@ class Excerpts
* @param PageInterface|null $page Page, defaults to the current page object
* @return Medium|Link
*/
public static function processMediaActions($medium, $url, PageInterface $page = null)
public static function processMediaActions($medium, $url, ?PageInterface $page = null)
{
$excerpts = new ExcerptsObject($page);

View File

@@ -60,9 +60,9 @@ class Truncator
$words = $currentWordPosition[2];
$curNode->nodeValue = substr(
$curNode->nodeValue,
(string) $curNode->nodeValue,
0,
$words[$offset][1] + strlen($words[$offset][0])
$words[$offset][1] + strlen((string) $words[$offset][0])
);
self::removeProceedingNodes($curNode, $container);
@@ -110,7 +110,7 @@ class Truncator
// If we have exceeded the limit, we want to delete the remainder of this document.
if ($letters->key() >= $limit) {
$currentText = $letters->currentTextPosition();
$currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
$currentText[0]->nodeValue = mb_substr((string) $currentText[0]->nodeValue, 0, $currentText[1] + 1);
self::removeProceedingNodes($currentText[0], $container);
if (!empty($ellipsis)) {
@@ -216,7 +216,7 @@ class Truncator
*/
private static function insertEllipsis($domNode, $ellipsis)
{
$avoid = array('a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5'); //html tags to avoid appending the ellipsis to
$avoid = ['a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5']; //html tags to avoid appending the ellipsis to
if ($domNode->parentNode->parentNode !== null && in_array($domNode->parentNode->nodeName, $avoid, true)) {
// Append as text node to parent instead
@@ -231,7 +231,7 @@ class Truncator
}
} else {
// Append to current node
$domNode->nodeValue = rtrim($domNode->nodeValue) . $ellipsis;
$domNode->nodeValue = rtrim((string) $domNode->nodeValue) . $ellipsis;
}
}
@@ -252,7 +252,7 @@ class Truncator
) {
if ($considerHtml) {
// if the plain text is shorter than the maximum length, return the whole text
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
if (strlen((string) preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text;
}
@@ -284,7 +284,7 @@ class Truncator
$truncate .= $line_matchings[1];
}
// calculate the length of the plain text part of the line; handle entities as one character
$content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
$content_length = strlen((string) preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
if ($total_length+$content_length> $length) {
// the number of characters which are left
$left = $length - $total_length;

View File

@@ -29,7 +29,7 @@ class YamlLinter
* @param string|null $folder
* @return array
*/
public static function lint(string $folder = null)
public static function lint(?string $folder = null)
{
if (null !== $folder) {
$folder = $folder ?: GRAV_ROOT;

View File

@@ -72,7 +72,7 @@ class Inflector
if (is_array(static::$uncountable)) {
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
if (substr($lowercased_word, -1 * strlen((string) $_uncountable)) === $_uncountable) {
return $word;
}
}
@@ -81,7 +81,7 @@ class Inflector
if (is_array(static::$irregular)) {
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr((string) $_singular, 1), $word);
}
}
}
@@ -89,7 +89,7 @@ class Inflector
if (is_array(static::$plural)) {
foreach (static::$plural as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
return preg_replace($rule, (string) $replacement, $word);
}
}
}
@@ -117,7 +117,7 @@ class Inflector
if (is_array(static::$uncountable)) {
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
if (substr($lowercased_word, -1 * strlen((string) $_uncountable)) === $_uncountable) {
return $word;
}
}
@@ -126,7 +126,7 @@ class Inflector
if (is_array(static::$irregular)) {
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr((string) $_plural, 1), $word);
}
}
}
@@ -134,7 +134,7 @@ class Inflector
if (is_array(static::$singular)) {
foreach (static::$singular as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
return preg_replace($rule, (string) $replacement, $word);
}
}
}
@@ -186,7 +186,7 @@ class Inflector
*/
public static function camelize($word)
{
return str_replace(' ', '', ucwords(preg_replace('/[^\p{L}^0-9]+/', ' ', $word)));
return str_replace(' ', '', ucwords((string) preg_replace('/[^\p{L}^0-9]+/', ' ', $word)));
}
/**
@@ -203,10 +203,10 @@ class Inflector
public static function underscorize($word)
{
$regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word);
$regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1);
$regex3 = preg_replace('/[^\p{L}^0-9]+/u', '_', $regex2);
$regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', (string) $regex1);
$regex3 = preg_replace('/[^\p{L}^0-9]+/u', '_', (string) $regex2);
return strtolower($regex3);
return strtolower((string) $regex3);
}
/**
@@ -223,11 +223,11 @@ class Inflector
public static function hyphenize($word)
{
$regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word);
$regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1);
$regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2);
$regex4 = preg_replace('/[^\p{L}^0-9]+/', '-', $regex3);
$regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', (string) $regex1);
$regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', (string) $regex2);
$regex4 = preg_replace('/[^\p{L}^0-9]+/', '-', (string) $regex3);
$regex4 = trim($regex4, '-');
$regex4 = trim((string) $regex4, '-');
return strtolower($regex4);
}
@@ -326,16 +326,12 @@ class Inflector
return $number . static::$ordinals['default'];
}
switch ($number % 10) {
case 1:
return $number . static::$ordinals['first'];
case 2:
return $number . static::$ordinals['second'];
case 3:
return $number . static::$ordinals['third'];
default:
return $number . static::$ordinals['default'];
}
return match ($number % 10) {
1 => $number . static::$ordinals['first'],
2 => $number . static::$ordinals['second'],
3 => $number . static::$ordinals['third'],
default => $number . static::$ordinals['default'],
};
}
/**

View File

@@ -24,7 +24,7 @@ use function is_object;
* Class Iterator
* @package Grav\Common
*/
class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable, \Stringable
{
use Constructor, ArrayAccessWithGetters, ArrayIterator, Countable, Serializable, Export;
@@ -35,11 +35,10 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Convert function calls for the existing keys into their values.
*
* @param string $key
* @param mixed $args
* @return mixed
*/
#[\ReturnTypeWillChange]
public function __call($key, $args)
public function __call($key, mixed $args)
{
return $this->items[$key] ?? null;
}
@@ -63,7 +62,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* @return string
*/
#[\ReturnTypeWillChange]
public function __toString()
public function __toString(): string
{
return implode(',', $this->items);
}
@@ -143,7 +142,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
*
* @return string|int|false Key if found, otherwise false.
*/
public function indexOf($needle)
public function indexOf(mixed $needle)
{
foreach (array_values($this->items) as $key => $value) {
if ($value === $needle) {
@@ -230,7 +229,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
*
* @return $this
*/
public function filter(callable $callback = null)
public function filter(?callable $callback = null)
{
foreach ($this->items as $key => $value) {
if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) {
@@ -250,7 +249,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* @return $this|array
*
*/
public function sort(callable $callback = null, $desc = false)
public function sort(?callable $callback = null, $desc = false)
{
if (!$callback || !is_callable($callback)) {
return $this;

View File

@@ -149,9 +149,7 @@ class Language
{
$languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(static function ($value) use ($delimiter) {
return preg_quote($value, $delimiter);
}, $languagesArray);
$languagesArray = array_map(static fn($value) => preg_quote((string) $value, $delimiter), $languagesArray);
sort($languagesArray);
@@ -358,7 +356,7 @@ class Language
* @param bool $assoc Return values in ['en' => '.en.md', ...] format.
* @return array Key is the language code, value is the file extension to be used.
*/
public function getFallbackPageExtensions(string $fileExtension = null, string $languageCode = null, bool $assoc = false)
public function getFallbackPageExtensions(?string $fileExtension = null, ?string $languageCode = null, bool $assoc = false)
{
$fileExtension = $fileExtension ?: CONTENT_EXT;
$key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc;
@@ -411,7 +409,7 @@ class Language
* @param bool $includeDefault If true, list contains '', which can be used for default
* @return array
*/
public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false)
public function getFallbackLanguages(?string $languageCode = null, bool $includeDefault = false)
{
// Handle default.
if ($languageCode === '' || !$this->enabled()) {
@@ -489,7 +487,7 @@ class Language
* @param bool $html_out
* @return string|string[]
*/
public function translate($args, array $languages = null, $array_support = false, $html_out = false)
public function translate($args, ?array $languages = null, $array_support = false, $html_out = false)
{
if (is_array($args)) {
$lookup = array_shift($args);
@@ -593,7 +591,7 @@ class Language
*/
public function getBrowserLanguages($accept_langs = [])
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED);
if (empty($this->http_accept_language)) {
if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
@@ -604,7 +602,7 @@ class Language
$langs = [];
foreach (explode(',', $accept_langs) as $k => $pref) {
foreach (explode(',', (string) $accept_langs) as $k => $pref) {
// split $pref again by ';q='
// and decorate the language entries by inverted position
if (false !== ($i = strpos($pref, ';q='))) {

View File

@@ -35,7 +35,7 @@ class Parsedown extends \Parsedown
$defaults = ['markdown' => $defaults];
}
$excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . self::class . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
}
$this->init($excerpts, $defaults);

View File

@@ -36,7 +36,7 @@ class ParsedownExtra extends \ParsedownExtra
$defaults = ['markdown' => $defaults];
}
$excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . self::class . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
}
parent::__construct();

View File

@@ -49,7 +49,7 @@ trait ParsedownGravTrait
$defaults = ['markdown' => $defaults];
}
$this->excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
user_error(self::class . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
} else {
$this->excerpts = $excerpts;
}
@@ -133,7 +133,7 @@ trait ParsedownGravTrait
array_splice($this->InlineTypes[$type], $index, 0, [$tag]);
}
if (strpos($this->inlineMarkerList, $type) === false) {
if (!str_contains($this->inlineMarkerList, $type)) {
$this->inlineMarkerList .= $type;
}
}
@@ -199,7 +199,7 @@ trait ParsedownGravTrait
*/
protected function blockTwigTag($line)
{
if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $line['body'], $matches)) {
if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', (string) $line['body'], $matches)) {
return ['markup' => $line['body']];
}
@@ -212,7 +212,7 @@ trait ParsedownGravTrait
*/
protected function inlineSpecialCharacter($excerpt)
{
if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $excerpt['text'])) {
if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', (string) $excerpt['text'])) {
return [
'markup' => '&amp;',
'extent' => 1,
@@ -235,7 +235,7 @@ trait ParsedownGravTrait
*/
protected function inlineImage($excerpt)
{
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
if (preg_match($this->twig_link_regex, (string) $excerpt['text'], $matches)) {
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
$excerpt = parent::inlineImage($excerpt);
$excerpt['element']['attributes']['src'] = $matches[1];
@@ -264,7 +264,7 @@ trait ParsedownGravTrait
$type = $excerpt['type'] ?? 'link';
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
if (preg_match($this->twig_link_regex, (string) $excerpt['text'], $matches)) {
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
$excerpt = parent::inlineLink($excerpt);
$excerpt['element']['attributes']['href'] = $matches[1];

Some files were not shown because too many files have changed in this diff Show More