Compare commits

..

274 Commits

Author SHA1 Message Date
Andy Miller
528032b11a update changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-29 21:18:57 -07:00
Andy Miller
a4c3a3af6d Add isindex to XSS dangerous tags (CVE-2023-31506 / GHSA-h85h-xm8x-vfw7)
The original CVE-2023-31506 fix missed the deprecated <isindex> HTML tag,
which can still be used for XSS via event handlers like onmouseover.

The <isindex> tag is deprecated in HTML5 and has no legitimate modern use.
2025-11-29 21:07:23 -07:00
Andy Miller
b7e1958a6e Merge branch 'fix/GHSA-4cwq-j7jv-qmwg-title-email-leak' into 1.8 2025-11-29 18:29:39 -07:00
Andy Miller
0c38968c58 Fix email disclosure in user edit page title (GHSA-4cwq-j7jv-qmwg)
Security fix for IDOR-style information disclosure where the admin
email address was leaked in the <title> tag even on 403 Forbidden
responses.

The edit view title template previously included the email:
  {{ fullname ?? username }} <{{ email }}>

Now shows only the name/username without email:
  {{ fullname ?? username }}

This prevents low-privilege users from enumerating admin email
addresses by accessing /admin/accounts/users/{username} URLs.
2025-11-29 18:27:08 -07:00
Andy Miller
9d11094e41 Merge branch 'fix/GHSA-x62q-p736-3997-GHSA-gq3g-666w-7h85-admin-security' into 1.8 2025-11-29 17:52:03 -07:00
Andy Miller
ed640a1314 Merge branch 'fix/GHSA-p4ww-mcp9-j6f2-GHSA-m8vh-v6r6-w7p6-GHSA-j422-qmxp-hv94-file-path-security' into 1.8 2025-11-29 17:45:33 -07:00
Andy Miller
e37259527d Merge branch 'fix/GHSA-662m-56v4-3r8f-GHSA-858q-77wx-hhx6-GHSA-8535-hvm8-2hmv-GHSA-gjc5-8cfh-653x-GHSA-52hh-vxfw-p6rg-ssti-sandbox' into 1.8 2025-11-29 17:30:39 -07:00
Andy Miller
3462d94d57 Merge branch 'fix/GHSA-h756-wh59-hhjv-GHSA-cjcp-qxvg-4rjm-username-validation' into 1.8 2025-11-29 17:29:15 -07:00
Andy Miller
19c2f8da76 Fix path traversal and uniqueness vulnerabilities in username validation
Security fixes for:
- GHSA-h756-wh59-hhjv: Path traversal via username during account creation
- GHSA-cjcp-qxvg-4rjm: Username uniqueness bypass

Changes:

Framework/Flex/Storage/AbstractFilesystemStorage.php:
- Added validateKey() to check for path traversal attempts
- Blocks: .., /, \, null bytes, control characters
- Added assertValidKey() public method for external validation

Framework/Flex/Storage/FolderStorage.php:
- Key validation now enforced in createRows()

User/DataUser/User.php:
- Added isValidUsername() static method for reusable validation
- Added uniqueness check in save() - blocks if user already exists
- Validation blocks: path traversal, hidden files, dangerous characters

Flex/Types/Users/UserObject.php:
- Validation now blocks hidden files (starting with .)

Added unit tests:
- tests/unit/Grav/Common/Security/UsernameValidationTest.php
- 50 tests covering path traversal, dangerous characters, and valid usernames
2025-11-29 17:25:02 -07:00
Andy Miller
a161399c84 Fix DoS via cron expressions and password hash exposure
Security fixes for:
- GHSA-x62q-p736-3997: DoS via invalid cron expression in scheduler
- GHSA-gq3g-666w-7h85: Password hash exposure to frontend

Changes:

Scheduler/Job.php:
- Added try-catch around CronExpression::factory() to prevent DoS
- Added isValidCronExpression() static validation method
- Returns null instead of throwing on invalid expressions

Scheduler/IntervalTrait.php:
- Added try-catch in at() method for graceful handling

Twig/Extension/GravExtension.php:
- Protected cronFunc() from invalid expressions

Console/Cli/SchedulerCommand.php:
- Handle null cron expressions (shows "Invalid cron" error)

Flex/Types/Users/UserObject.php:
- Override jsonSerialize() to filter out hashed_password, secret, twofa_secret
- Prevents sensitive data from being exposed to frontend/HTML

User/DataUser/User.php:
- Same jsonSerialize() override for DataUser implementation

Added unit tests:
- tests/unit/Grav/Common/Security/AdminSecurityTest.php
- 53 tests covering cron validation and password hash protection
2025-11-29 17:24:41 -07:00
Andy Miller
5f120c328b Fix file read, DoS, and path traversal vulnerabilities
Security fixes for:
- GHSA-p4ww-mcp9-j6f2: Arbitrary file read via read_file() Twig function
- GHSA-m8vh-v6r6-w7p6: DoS via malformed language code in regex
- GHSA-j422-qmxp-hv94: Path traversal in backup root configuration

Changes:

GravExtension.php - readFileFunc():
- Added realpath validation to prevent path traversal
- Blocked reading files outside GRAV_ROOT
- Blocked sensitive files: accounts/*.yaml, .env, .git, logs, backups, vendor

Language.php:
- Fixed regex delimiter in setActiveFromUri() to properly escape language codes
- Added validation in setLanguages() to only allow valid language codes
- Pattern: /^[a-zA-Z]{2,3}(?:[-_][a-zA-Z0-9]{2,8})?$/

Backups.php:
- Added path traversal protection with realpath validation
- Blocked access to system directories: /etc, /root, /home, /var, etc.

Added unit tests:
- tests/unit/Grav/Common/Security/FilePathSecurityTest.php
- 55 tests covering language code validation and regex injection prevention
2025-11-29 17:24:22 -07:00
Andy Miller
db924c4a26 Expand SSTI sandbox blacklist to block known attack vectors
Security fixes for:
- GHSA-662m-56v4-3r8f: SSTI sandbox bypass via nested evaluate_twig
- GHSA-858q-77wx-hhx6: Privilege escalation via grav.user/scheduler
- GHSA-8535-hvm8-2hmv: Context leak via Forms _context access
- GHSA-gjc5-8cfh-653x: Sandbox bypass via grav.config.set
- GHSA-52hh-vxfw-p6rg: CVE-2024-28116 bypass via string concatenation

Changes to cleanDangerousTwig():
- Added 150+ dangerous PHP functions to blacklist
- Blocked access to grav.scheduler, grav.twig.twig, grav.backups, grav.gpm
- Blocked config modification via config.set()
- Blocked user modification via grav.user.update()/save()
- Blocked context/internal access via _context, _self, twig_vars
- Blocked evaluate_twig/evaluate to prevent nested bypass
- Added string concatenation pattern detection for bypass attempts
- Blocked SSRF vectors (curl, fsockopen, stream_socket)
- Blocked file operations, serialization, reflection classes

Performance optimizations:
- Early exit if string has no Twig blocks ({{ or {%})
- Static caching of compiled regex patterns (built once, reused)
- Combined all patterns into 4 single regex operations instead of ~190 loops
- Consolidated property patterns using regex alternation groups

Added unit tests:
- tests/unit/Grav/Common/Security/CleanDangerousTwigTest.php
- 104 tests covering all GHSA advisories and dangerous patterns
2025-11-29 17:24:06 -07:00
Andy Miller
9fc1b42d59 prepare beta release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-29 11:02:20 -07:00
Andy Miller
c8878dfc80 upgrade to symfony 7.4 stable
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-29 10:58:03 -07:00
Andy Miller
779661ab8a more improvements for JS minification and now pulls any broken JS out of pipeline
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-27 20:56:07 +00:00
Andy Miller
3985638a8f more debug in the Pipeline.php to identify issues
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-27 19:19:02 +00:00
Andy Miller
a78789b291 upgrade compoer libs
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 21:05:46 +00:00
Andy Miller
caa127cd53 disallow xref/xhref in SVGs
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-24 21:04:44 +00:00
Andy Miller
5f087d3a43 fix range requests for partial content in Utils::downloads() - Fixes #3990
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-23 17:55:28 +00:00
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
358 changed files with 12835 additions and 6886 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)

4
.gitignore vendored
View File

@@ -48,7 +48,7 @@ tests/cache/*
tests/error.log
system/templates/testing/*
/user/config/versions.yaml
/system/recovery.window
/user/data/recovery.window
tmp/*
#needs_fixing.txt
/AGENTS.md
/.claude

View File

@@ -1,36 +1,80 @@
# v1.7.50.9
## 11/09/2025
# v1.8.0-beta.27
## mm/dd/2025
1. [](#improved)
* Better error warnings regarding upgrading from 1.7 -> 1.7 vs 1.7 -> 1.8
* Hardened Twig sandbox with expanded blacklist blocking 150+ dangerous functions and attack patterns
* Added static regex caching in Security class for improved performance
* Added path traversal protection to backup root configuration
* Added validation for language codes to prevent regex injection DoS
1. [](#bugfix)
* Fix for update-provided `Install.php` not used if local version called first
* Fix class loading error when trying to use `bin/gpm self-upgrade --safe`
* Fixed path traversal vulnerability in username during account creation
* Fixed username uniqueness bypass allowing duplicate accounts
* Fixed arbitrary file read via `read_file()` Twig function
* Fixed DoS via malformed cron expressions in scheduler
* Fixed password hash exposure to frontend via JSON serialization
* Fixed email disclosure in user edit page title
* Fixed XSS via `isindex` tag bypass (CVE-2023-31506)
# v1.7.50.8
# v1.8.0-beta.26
## 11/29/2025
1. [](#improved)
* Improvements for JS minification and now pulls any broken JS out of pipeline
* Disallow xref/xhref in SVGs
* Upgraded to recently released Symfony 7.4
1. [](#bugfix)
* fix range requests for partial content in Utils::downloads() - Fixes [#3990](https://github.com/getgrav/grav-plugin-admin/issues/3990)
# v1.8.0-beta.25
## 11/22/2025
1. [](#bugfix)
* Fixed Twig version
# v1.8.0-beta.24
## 11/20/2025
1. [](#improved)
* More Twig3 compatibility fixes and tests
* Changed snapshot creationg to use copy instead of move for improved reliability
* Lazy load page optimization
* Regex caching optimization
* Gated Debugger `addEvent()` optimization
* Various SafeUpgrade performance optimizations
* Improved Twig Deferred block implementation
1. [](#bugfix)
* Fix various Twig3 deprecated notices
* Fixed slow purge snapshot functionality and test
# v1.8.0-beta.23
## 11/14/2025
1. [](#improved)
* Refactored safe-upgrade from scratch with simplified 'install' step
# v1.8.0-beta.22
## 11/06/2025
1. [](#bugfix)
* Removed over zealous safety checks
* Removed .gitattributes which was causing some unintended issues
# v1.7.50.7
# v1.8.0-beta.21
## 11/05/2025
1. [](#improved)
* Exclude dev files from exports
* Remove dev file in clean command
1. [](#bugfix)
* Ignore .github and .phan folders during self-upgrade
* Fixed path check in self-upgrade
# v1.7.50.6
# v1.8.0-beta.20
## 11/05/2025
1. [](#bugfix)
* Fixed an issue where non-upgradable root-level folders were snapshotted
# v1.7.50.5
# v1.8.0-beta.19
## 11/05/2025
1. [](#new)
@@ -38,42 +82,167 @@
* 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.7.50.4
# v1.8.0-beta.18
## 10/31/2025
1. [](#improved)
* More fixes and improvements for safe-uprade process
* 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
# v1.7.50.3
## 10/21/2025
1. [](#bugfix)
* Restored `user/config/system.yaml` to 1.7 branch version (testing mode off)
# v1.7.50.2
## 10/21/2025
1. [](#bugfix)
* Fix for `SafeUpgradeService::getLastManifest()` fatal error on upgrade [#3966](https://github.com/getgrav/grav/issues/3966)
# v1.7.50.1
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. [](#bugfix)
* Fix for broken `GRAV_ROOT`
1. [](#improved)
* Set `bin/*` binaries to `+x` permission when upgrading via CLI
* Improved Twig3 compatibility fixes
# v1.7.50
# 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 **Safe Core Upgrade** process with snapshots for backup and restore, better preflight and postflight checks, as well as exception checking post-install for easy rollback.
* 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
1. [](#new)
* Added staged self-upgrade pipeline with manifest snapshots and atomic swaps for Grav core updates.
* Introduced recovery mode with token-gated UI, plugin quarantine, and CLI rollback support.
* Added `bin/gpm preflight` compatibility scanner and `bin/gpm rollback` utility.
* Added `wordCount` Twig filter [#3957](https://github.com/getgrav/grav/pulls/3957)
# v1.7.49.5
## 09/10/2025
@@ -141,18 +310,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.

Binary file not shown.

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": {

4004
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,8 @@
namespace Grav;
\define('GRAV_REQUEST_TIME', microtime(true));
\define('GRAV_PHP_MIN', '7.3.6');
\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;
@@ -21,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) ?? '/';
@@ -36,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;
@@ -59,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());
@@ -88,7 +87,7 @@ 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 {
@@ -103,4 +102,3 @@ try {
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,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

View File

@@ -72,7 +72,7 @@ config:
# Edit view
edit:
title:
template: "{{ form.value('fullname') ?? form.value('username') }} &lt;{{ form.value('email') }}&gt;"
template: "{{ form.value('fullname') ?? form.value('username') }}"
# Configure view
configure:

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,50 @@
xss_whitelist: [admin.super] # Whitelist of user access that should 'skip' XSS checking
xss_whitelist:
- admin.super
xss_enabled:
on_events: true
invalid_protocols: true
moz_binding: true
html_inline_styles: true
dangerous_tags: true
on_events: true
invalid_protocols: true
moz_binding: true
html_inline_styles: true
dangerous_tags: true
xss_invalid_protocols:
- javascript
- livescript
- vbscript
- mocha
- feed
- data
- javascript
- livescript
- vbscript
- mocha
- feed
- data
xss_dangerous_tags:
- applet
- meta
- xml
- blink
- link
- style
- script
- embed
- object
- iframe
- frame
- frameset
- ilayer
- layer
- bgsound
- title
- base
- applet
- meta
- xml
- blink
- link
- style
- script
- embed
- object
- iframe
- frame
- frameset
- ilayer
- layer
- bgsound
- title
- base
- isindex
uploads_dangerous_extensions:
- php
- php2
- php3
- php4
- php5
- phar
- phtml
- html
- htm
- shtml
- shtm
- js
- exe
- php
- php2
- php3
- php4
- php5
- phar
- phtml
- html
- htm
- shtml
- shtm
- js
- exe
sanitize_svg: true
salt: SbmgUJQ62MqNc0

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.9');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);
define('GRAV_VERSION', '1.8.0-beta.27');
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

@@ -13,6 +13,13 @@ if (!defined('GRAV_ROOT')) {
// Check if Install class is already loaded (from an older Grav version)
// This happens when upgrading from older versions where the OLD Install class
// was loaded via autoloader before extracting the update package (e.g., via Install::forceSafeUpgrade())
$logInstallerSource = static function ($install, string $source) {
$sourceLabel = $source === 'extracted update package' ? 'update package' : 'existing installation';
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
echo sprintf(" |- Using installer from %s\n", $sourceLabel);
}
};
if (class_exists('Grav\\Installer\\Install', false)) {
// OLD Install class is already loaded. We cannot load the NEW one due to PHP limitations.
// However, we can work around this by:
@@ -31,10 +38,15 @@ if (class_exists('Grav\\Installer\\Install', false)) {
$locationProp->setValue($install, __DIR__ . '/..');
}
$logInstallerSource($install, 'existing installation');
return $install;
}
// Normal case: Install class not yet loaded, load the NEW one
require_once __DIR__ . '/src/Grav/Installer/Install.php';
return Grav\Installer\Install::instance();
$install = Grav\Installer\Install::instance();
$logInstallerSource($install, 'extracted update package');
return $install;

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

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;
}
@@ -474,7 +472,23 @@ class Assets extends PropertyObject
$group_attributes = array_merge($attributes, $pipeline_group['attributes']);
$pipeline = new Pipeline($options);
$pipeline_output .= $pipeline->$render_pipeline($pipeline_group['assets'], $group, $group_attributes);
$result = $pipeline->$render_pipeline($pipeline_group['assets'], $group, $group_attributes);
// Handle different return types from pipeline
if ($result === false) {
// No assets to render
continue;
} elseif (is_array($result)) {
// Array result contains pipelined output and any failed assets
$pipeline_output .= $result['output'];
// Render failed assets individually (they couldn't be minified)
foreach ($result['failed'] as $asset) {
$pipeline_output .= $asset->render();
}
} else {
// String result (no minification or CSS)
$pipeline_output .= $result;
}
}
} else {
foreach ($pipeline_assets as $asset) {
@@ -587,18 +601,11 @@ 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;
}

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

@@ -11,13 +11,14 @@ namespace Grav\Common\Assets;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Object\PropertyObject;
use MatthiasMullie\Minify\CSS;
use MatthiasMullie\Minify\JS;
use tubalmartin\CssMin\Minifier as CSSMinifier;
use JShrink\Minifier as JSMinifier;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function array_key_exists;
@@ -144,9 +145,8 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('css')) {
$minifier = new CSS();
$minifier->add($buffer);
$buffer = $minifier->minify();
$minifier = new CSSMinifier();
$buffer = $minifier->run($buffer);
}
// Write file
@@ -171,7 +171,9 @@ class Pipeline extends PropertyObject
* @param array $assets
* @param string $group
* @param array $attributes
* @return bool|string URL or generated content if available, else false
* @param int $type
* @return array{output: string, failed: array}|string|false Returns array with output and failed assets when minifying,
* string when not minifying, or false if no assets
*/
public function renderJs($assets, $group, $attributes = [], $type = self::JS_ASSET)
{
@@ -186,44 +188,55 @@ class Pipeline extends PropertyObject
// Store Attributes
$this->attributes = $attributes;
// Compute uid based on assets and timestamp
$json_assets = json_encode($assets);
$uid = md5($json_assets . $this->js_minify . $group);
$file = $uid . '.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$filepath = "{$this->assets_dir}/{$file}";
if (file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
return false;
}
// Concatenate files
$buffer = $this->gatherLinks($assets, $type);
// Minify if required
if ($this->shouldMinify('js')) {
$minifier = new JS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
// Write file
if (trim($buffer) !== '') {
file_put_contents($filepath, $buffer);
}
//if nothing found get out of here!
if (empty($assets)) {
return false;
}
if ($inline_group) {
$shouldMinify = $this->shouldMinify('js');
$failedAssets = [];
// When minifying, process each file individually to isolate failures
if ($shouldMinify) {
$result = $this->gatherAndMinifyJs($assets, $type);
$buffer = $result['buffer'];
$failedAssets = $result['failed'];
// Compute uid based on successful assets only
$successfulAssets = array_diff_key($assets, array_flip(array_keys($failedAssets)));
$json_assets = json_encode($successfulAssets);
} else {
$buffer = $this->gatherLinks($assets, $type);
$json_assets = json_encode($assets);
}
$uid = md5($json_assets . (int)$shouldMinify . $group);
$file = $uid . '.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$filepath = "{$this->assets_dir}/{$file}";
// Check for cached version (only if no failed assets, as cache key changes)
if (empty($failedAssets) && file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} elseif (trim($buffer) !== '') {
// Write file
file_put_contents($filepath, $buffer);
}
if (trim($buffer) === '') {
$output = '';
} elseif ($inline_group) {
$output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
} else {
$this->asset = $relative_path;
$output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . "></script>\n";
}
// Return array with failed assets if minifying, otherwise just the output string
if ($shouldMinify) {
return ['output' => $output, 'failed' => $failedAssets];
}
return $output;
}
@@ -283,7 +296,7 @@ class Pipeline extends PropertyObject
} else {
return str_replace($matches[2], $new_url, $matches[0]);
}
}, $file);
}, (string) $file);
return $file;
}
@@ -344,4 +357,75 @@ class Pipeline extends PropertyObject
return $minify;
}
/**
* Gather JS files and minify each one individually.
* Files that fail minification are tracked and returned separately.
*
* @param array $assets Array of asset objects
* @param int $type Asset type (JS_ASSET or JS_MODULE_ASSET)
* @return array{buffer: string, failed: array} Combined minified content and failed assets
*/
private function gatherAndMinifyJs(array $assets, int $type): array
{
$buffer = '';
$failed = [];
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
foreach ($assets as $key => $asset) {
$local = true;
$link = $asset->getAsset();
$relative_path = $link;
if (static::isRemoteLink($link)) {
$local = false;
if (str_starts_with((string) $link, '//')) {
$link = 'http:' . $link;
}
$relative_dir = dirname((string) $relative_path);
} else {
// Fix to remove relative dir if grav is in one
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
$base_url = '#' . preg_quote($this->base_url, '#') . '#';
$relative_path = ltrim((string) preg_replace($base_url, '/', (string) $link, 1), '/');
}
$relative_dir = dirname((string) $relative_path);
$link = GRAV_ROOT . '/' . $relative_path;
}
$file = $this->fetch_command instanceof \Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
// No file found, skip it...
if ($file === false) {
continue;
}
// Ensure proper termination
$file = rtrim((string) $file, ' ;') . ';';
// Rewrite imports for JS modules
if ($type === self::JS_MODULE_ASSET) {
$file = $this->jsRewrite($file, $relative_dir, $local);
}
// Try to minify this individual file
try {
$file = JSMinifier::minify($file);
$file = rtrim($file) . PHP_EOL;
$buffer .= $file;
} catch (\Exception $e) {
// Track failed asset for individual rendering
$failed[$key] = $asset;
$message = "JS Minification failed for '{$asset->getAsset()}': {$e->getMessage()}";
$debugger->addMessage($message, 'error');
Grav::instance()['log']->error($message);
}
}
return ['buffer' => $buffer, 'failed' => $failed];
}
}

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;
@@ -225,6 +225,25 @@ class Backups
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
}
// Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal
$realBackupRoot = realpath($backup_root);
$realGravRoot = realpath(GRAV_ROOT);
if ($realBackupRoot === false || $realGravRoot === false) {
throw new RuntimeException("Invalid backup location: {$backup_root}");
}
// Ensure the backup root is within GRAV_ROOT or a parent thereof (for backing up GRAV itself)
// Block access to system directories outside the web root
$blockedPaths = ['/etc', '/root', '/home', '/var', '/usr', '/bin', '/sbin', '/tmp', '/proc', '/sys', '/dev'];
foreach ($blockedPaths as $blocked) {
if (strpos($realBackupRoot, $blocked) === 0) {
throw new RuntimeException("Backup location not allowed: {$backup_root}");
}
}
$backup_root = $realBackupRoot;
$options = [
'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),
'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),

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
@@ -672,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;
}
@@ -349,6 +349,22 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $this->getGroups();
}
/**
* {@inheritdoc}
* Override to filter out sensitive fields like password hashes
*/
public function jsonSerialize(): array
{
$elements = parent::jsonSerialize();
// Security: Remove sensitive fields that should never be exposed to frontend
unset($elements['hashed_password']);
unset($elements['secret']); // 2FA secret
unset($elements['twofa_secret']); // Alternative 2FA field name
return $elements;
}
/**
* Convert object into an array.
*
@@ -401,7 +417,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 +573,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;
@@ -583,10 +599,19 @@ class UserObject extends FlexObject implements UserInterface, Countable
{
// TODO: We may want to handle this in the storage layer in the future.
$key = $this->getStorageKey();
if (!$key || strpos($key, '@@')) {
$isNewUser = !$key || strpos($key, '@@');
if ($isNewUser) {
$storage = $this->getFlexDirectory()->getStorage();
if ($storage instanceof FileStorage) {
$this->setStorageKey($this->getKey());
$newKey = $this->getKey();
// Check if a user with this username already exists (prevent overwriting)
if ($storage->hasKey($newKey)) {
throw new RuntimeException('User account with this username already exists');
}
$this->setStorageKey($newKey);
}
}
@@ -1027,10 +1052,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 +1064,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

@@ -39,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;
@@ -59,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();
@@ -67,7 +65,6 @@ class GPM extends Iterator
$this->cache = [];
$this->installed = new Local\Packages();
$this->refresh = $refresh;
$this->callback = $callback;
}
@@ -80,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),
};
}
/**
@@ -97,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),
};
}
/**
@@ -128,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);
@@ -288,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);
@@ -596,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;
@@ -612,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;
@@ -1266,7 +1259,7 @@ class GPM extends Iterator
*/
public function versionFormatIsNextSignificantRelease($version): bool
{
return strpos($version, '~') === 0;
return str_starts_with($version, '~');
}
/**
@@ -1279,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

@@ -45,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;
@@ -81,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

@@ -134,7 +134,17 @@ class Language
*/
public function setLanguages($langs)
{
$this->languages = $langs;
// Validate and sanitize language codes to prevent regex injection
$validLangs = [];
foreach ((array)$langs as $lang) {
$lang = (string)$lang;
// Only allow valid language codes (alphanumeric, hyphens, underscores)
// Examples: en, en-US, en_US, zh-Hans, pt-BR
if (preg_match('/^[a-zA-Z]{2,3}(?:[-_][a-zA-Z0-9]{2,8})?$/', $lang)) {
$validLangs[] = $lang;
}
}
$this->languages = $validLangs;
$this->init();
}
@@ -149,9 +159,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);
@@ -236,7 +244,8 @@ class Language
*/
public function setActiveFromUri($uri)
{
$regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i';
// Pass delimiter '/' to getAvailable() to properly escape language codes for regex
$regex = '/(^\/(' . $this->getAvailable('/') . '))(?:\/|\?|$)/i';
// if languages set
if ($this->enabled()) {
@@ -358,7 +367,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 +420,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 +498,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 +602,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 +613,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];

View File

@@ -105,7 +105,7 @@ interface MediaCollectionInterface extends \Grav\Framework\Media\Interfaces\Medi
* @param Blueprint|null $blueprint
* @return Medium|null
*/
public function createFromArray(array $items = [], Blueprint $blueprint = null);
public function createFromArray(array $items = [], ?Blueprint $blueprint = null);
/**
* @param MediaObjectInterface $mediaObject

View File

@@ -207,11 +207,10 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj
* Allow any action to be called on this medium from twig or markdown
*
* @param string $method
* @param mixed $args
* @return $this
*/
#[\ReturnTypeWillChange]
public function __call($method, $args);
public function __call($method, mixed $args);
/**
* Set value by using dot notation for nested arrays/objects.
@@ -223,5 +222,5 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj
* @param string|null $separator Separator, defaults to '.'
* @return $this
*/
public function set($name, $value, $separator = null);
public function set($name, mixed $value, $separator = null);
}

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