diff --git a/CHANGELOG.md b/CHANGELOG.md index 570b13482b..5a6091c73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# v1.7.21 +## 09/14/2021 + +1. [](#new) + * Added `|yaml` filter to convert input to YAML + * Added `route` and `request` to `onPageNotFound` event + * Added file upload/remove support for `Flex Forms` + * Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints + * Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()` + * Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages +2. [](#improved) + * Use a simplified text-based `cron` field for scheduler + * Add timestamp to logging output of scheduler jobs to see when they ran +3. [](#bugfix) + * Fixed escaping in PageIndex::getLevelListing() + * Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433) + * Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432) + * Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439) + * Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie + # v1.7.20 ## 09/01/2021 diff --git a/composer.lock b/composer.lock index f06c4a6010..8d2907f419 100644 --- a/composer.lock +++ b/composer.lock @@ -311,26 +311,26 @@ }, { "name": "doctrine/collections", - "version": "1.6.7", + "version": "1.6.8", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a" + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/55f8b799269a1a472457bd1a41b4f379d4cfba4a", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", "shasum": "" }, "require": { "php": "^7.1.3 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan-shim": "^0.9.2", - "phpunit/phpunit": "^7.0", - "vimeo/psalm": "^3.8.1" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", + "vimeo/psalm": "^4.2.1" }, "type": "library", "autoload": { @@ -374,9 +374,9 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.6.7" + "source": "https://github.com/doctrine/collections/tree/1.6.8" }, - "time": "2020-07-27T17:53:49+00:00" + "time": "2021-08-10T18:51:53+00:00" }, { "name": "donatj/phpuseragentparser", @@ -642,16 +642,16 @@ }, { "name": "filp/whoops", - "version": "2.14.0", + "version": "2.14.1", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b" + "reference": "15ead64e9828f0fc90932114429c4f7923570cb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/fdf92f03e150ed84d5967a833ae93abffac0315b", - "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b", + "url": "https://api.github.com/repos/filp/whoops/zipball/15ead64e9828f0fc90932114429c4f7923570cb1", + "reference": "15ead64e9828f0fc90932114429c4f7923570cb1", "shasum": "" }, "require": { @@ -701,7 +701,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.0" + "source": "https://github.com/filp/whoops/tree/2.14.1" }, "funding": [ { @@ -709,7 +709,7 @@ "type": "github" } ], - "time": "2021-07-13T12:00:00+00:00" + "time": "2021-08-29T12:00:00+00:00" }, { "name": "getgrav/cache", @@ -2258,16 +2258,16 @@ }, { "name": "symfony/console", - "version": "v4.4.29", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b" + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", + "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22", + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22", "shasum": "" }, "require": { @@ -2328,7 +2328,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.29" + "source": "https://github.com/symfony/console/tree/v4.4.30" }, "funding": [ { @@ -2344,7 +2344,7 @@ "type": "tidelift" } ], - "time": "2021-07-27T19:04:53+00:00" + "time": "2021-08-25T19:27:26+00:00" }, { "name": "symfony/contracts", @@ -2442,16 +2442,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9" + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/958a128b184fcf0ba45ec90c0e88554c9327c2e9", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac", + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac", "shasum": "" }, "require": { @@ -2506,7 +2506,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.27" + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30" }, "funding": [ { @@ -2522,7 +2522,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/http-client", @@ -3009,16 +3009,16 @@ }, { "name": "symfony/process", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f" + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", + "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", "shasum": "" }, "require": { @@ -3051,7 +3051,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.27" + "source": "https://github.com/symfony/process/tree/v4.4.30" }, "funding": [ { @@ -3067,20 +3067,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba" + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", "shasum": "" }, "require": { @@ -3140,7 +3140,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.27" + "source": "https://github.com/symfony/var-dumper/tree/v4.4.30" }, "funding": [ { @@ -3156,7 +3156,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/yaml", @@ -3580,20 +3580,20 @@ }, { "name": "codeception/lib-innerbrowser", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1" + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/4b0d89b37fe454e060a610a85280a87ab4f534f1", - "reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", + "codeception/codeception": "4.*@dev", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", @@ -3634,9 +3634,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.0" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1" }, - "time": "2021-04-23T06:18:29+00:00" + "time": "2021-08-30T15:21:42+00:00" }, { "name": "codeception/module-asserts", @@ -4569,16 +4569,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.94", + "version": "0.12.98", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6" + "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6", - "reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3bb7cc246c057405dd5e290c3ecc62ab51d57e00", + "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00", "shasum": "" }, "require": { @@ -4609,7 +4609,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.94" + "source": "https://github.com/phpstan/phpstan/tree/0.12.98" }, "funding": [ { @@ -4629,7 +4629,7 @@ "type": "tidelift" } ], - "time": "2021-07-30T09:05:27+00:00" + "time": "2021-09-02T12:33:01+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -5002,16 +5002,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.8", + "version": "9.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb" + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/191768ccd5c85513b4068bdbe99bb6390c7d54fb", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", "shasum": "" }, "require": { @@ -5089,7 +5089,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9" }, "funding": [ { @@ -5101,7 +5101,7 @@ "type": "github" } ], - "time": "2021-07-31T15:17:34+00:00" + "time": "2021-08-31T06:47:40+00:00" }, { "name": "psr/http-client", @@ -6008,6 +6008,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { @@ -6326,16 +6327,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.3.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f" + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2dd8890bd01be59a5221999c05ccf0fcafcb354f", - "reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", "shasum": "" }, "require": { @@ -6381,7 +6382,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.3.4" + "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7" }, "funding": [ { @@ -6397,20 +6398,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:55:36+00:00" + "time": "2021-08-29T19:32:13+00:00" }, { "name": "symfony/finder", - "version": "v5.3.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "17f50e06018baec41551a71a15731287dbaab186" + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/17f50e06018baec41551a71a15731287dbaab186", - "reference": "17f50e06018baec41551a71a15731287dbaab186", + "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93", "shasum": "" }, "require": { @@ -6443,7 +6444,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.4" + "source": "https://github.com/symfony/finder/tree/v5.3.7" }, "funding": [ { @@ -6459,7 +6460,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:54:19+00:00" + "time": "2021-08-04T21:20:46+00:00" }, { "name": "theseer/tokenizer", diff --git a/system/blueprints/config/scheduler.yaml b/system/blueprints/config/scheduler.yaml index caa7711678..a8dce314bd 100644 --- a/system/blueprints/config/scheduler.yaml +++ b/system/blueprints/config/scheduler.yaml @@ -47,7 +47,8 @@ form: label: PLUGIN_ADMIN.EXTRA_ARGUMENTS placeholder: '-lah' .at: - type: cron + type: text + wrapper_classes: cron-selector label: PLUGIN_ADMIN.SCHEDULER_RUNAT help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP placeholder: '* * * * *' diff --git a/system/defines.php b/system/defines.php index 9844b6b885..9ab384faba 100644 --- a/system/defines.php +++ b/system/defines.php @@ -9,7 +9,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.7.20'); +define('GRAV_VERSION', '1.7.21'); define('GRAV_SCHEMA', '1.7.0_2020-11-20_1'); define('GRAV_TESTING', false); diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php index 83a2763951..6ff7372d0b 100644 --- a/system/src/Grav/Common/Config/Setup.php +++ b/system/src/Grav/Common/Config/Setup.php @@ -400,8 +400,12 @@ protected function check(UniformResourceLocator $locator) $this->initializeLocator($locator); } - // Create security.yaml if it doesn't exist. - $filename = $locator->findResource(static::$securityFile, true, true); + // Create security.yaml salt if it doesn't exist into existing configuration environment if possible. + $securityFile = basename(static::$securityFile); + $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile)); + $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true); + $filename = "{$securityFolder}/{$securityFile}"; + $security_file = CompiledYamlFile::instance($filename); $security_content = (array)$security_file->content(); diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index f69c841fc0..7ee60e4155 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -538,8 +538,10 @@ public static function typeNumber($value, array $params, array $field) if (isset($params['step'])) { $step = (float)$params['step']; + // Count of how many steps we are above/below the minimum value. + $pos = ($value - $min) / $step; - return fmod($value - $min, $step) === 0.0; + return is_int(static::filterNumber($pos, $params, $field)); } return true; diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index e2938aeb48..dd06f58688 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -674,12 +674,12 @@ protected function getLevelListingRecurse(array $options): array $count = $filters ? $tmp->filterBy($filters, true)->count() : null; $route = $child->getRoute(); $payload = [ - 'item-key' => basename($child->rawRoute() ?? $child->getKey()), + 'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())), 'icon' => $icon, 'title' => htmlspecialchars($child->menu()), 'route' => [ - 'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '', - 'raw' => $child->rawRoute(), + 'display' => htmlspecialchars(($route ? ($route->toString(false) ?: '/') : null) ?? ''), + 'raw' => htmlspecialchars($child->rawRoute()), ], 'modified' => $this->jsDate($child->modified()), 'child_count' => $child_count ?: null, diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php index fb69eab68f..ea68fa1bf8 100644 --- a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php +++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php @@ -41,6 +41,14 @@ public static function getCachedMethods(): array ] + parent::getCachedMethods(); } + /** + * @return string + */ + public function getTitle(): string + { + return $this->getProperty('readableName'); + } + /** * Checks user authorization to the action. * diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php index 82b9f7c4f1..310c313fe6 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserObject.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -32,6 +32,7 @@ use Grav\Common\User\Traits\UserTrait; use Grav\Framework\File\Formatter\JsonFormatter; use Grav\Framework\File\Formatter\YamlFormatter; +use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Flex\Flex; use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\Storage\FileStorage; @@ -305,6 +306,14 @@ public function getProperty($property, $default = null) return $value; } + /** + * @return UserGroupIndex + */ + public function getRoles(): UserGroupIndex + { + return $this->getGroups(); + } + /** * Convert object into an array. * @@ -702,6 +711,7 @@ protected function getOriginalMedia() /** * @param array $files + * @return void */ protected function setUpdatedMedia(array $files): void { @@ -713,9 +723,12 @@ protected function setUpdatedMedia(array $files): void return; } + $filesystem = Filesystem::getInstance(false); + $list = []; $list_original = []; foreach ($files as $field => $group) { + // Ignore files without a field. if ($field === '') { continue; } @@ -737,7 +750,7 @@ protected function setUpdatedMedia(array $files): void } if ($file) { - // Check file upload against media limits. + // Check file upload against media limits (except for max size). $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings); } @@ -761,15 +774,19 @@ protected function setUpdatedMedia(array $files): void continue; } + // Calculate path without the retina scaling factor. + $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath)); + $list[$filename] = [$file, $settings]; + $path = str_replace('.', "\n", $field); if (null !== $data) { $data['name'] = $filename; $data['path'] = $filepath; - $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n"); + $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n"); } else { - $this->unsetNestedProperty("{$field}\n{$filepath}", "\n"); + $this->unsetNestedProperty("{$path}\n{$realpath}", "\n"); } } } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index c9097eea8a..ffbb508183 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -9,6 +9,7 @@ namespace Grav\Common; +use Composer\Autoload\ClassLoader; use Grav\Common\Config\Config; use Grav\Common\Config\Setup; use Grav\Common\Helpers\Exif; @@ -152,6 +153,13 @@ public static function instance(array $values = []) { if (null === self::$instance) { self::$instance = static::load($values); + + /** @var ClassLoader|null $loader */ + $loader = self::$instance['loader'] ?? null; + if ($loader) { + // Load fix for Deferred Twig Extension + $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true); + } } elseif ($values) { $instance = self::$instance; foreach ($values as $key => $value) { diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php index d5d1d46678..470ca907b5 100644 --- a/system/src/Grav/Common/Processors/PagesProcessor.php +++ b/system/src/Grav/Common/Processors/PagesProcessor.php @@ -10,6 +10,7 @@ namespace Grav\Common\Processors; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Framework\RequestHandler\Exception\RequestException; use Grav\Plugin\Form\Forms; use RocketTheme\Toolbox\Event\Event; use Psr\Http\Message\ResponseInterface; @@ -48,8 +49,17 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $page = $this->container['page']; if (!$page->routable()) { + $exception = new RequestException($request, 'Page Not Found', 404); + $route = $this->container['route']; // If no page found, fire event - $event = new Event(['page' => $page]); + $event = new Event([ + 'page' => $page, + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'exception' => $exception, + 'route' => $route, + 'request' => $request + ]); $event->page = null; $event = $this->container->fireEvent('onPageNotFound', $event); diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php index f21c26defd..94fbaf102c 100644 --- a/system/src/Grav/Common/Scheduler/Job.php +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -390,7 +390,9 @@ private function postRun() if (count($this->outputTo) > 0) { foreach ($this->outputTo as $file) { $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX; - file_put_contents($file, $this->output, $output_mode); + $timestamp = (new DateTime('now'))->format('c'); + $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output; + file_put_contents($file, $output, $output_mode); } } diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index 8856e7a0a4..2dbc3073ad 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -12,6 +12,7 @@ use Grav\Common\Form\FormFlash; use Grav\Events\SessionStartEvent; use Grav\Plugin\Form\Forms; +use JsonException; use function is_string; /** @@ -148,10 +149,11 @@ public function getFlashObject($name) * @param mixed $object * @param int $time * @return $this + * @throws JsonException */ public function setFlashCookieObject($name, $object, $time = 60) { - setcookie($name, json_encode($object), time() + $time, '/'); + setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time)); return $this; } @@ -161,13 +163,15 @@ public function setFlashCookieObject($name, $object, $time = 60) * * @param string $name * @return mixed|null + * @throws JsonException */ public function getFlashCookieObject($name) { if (isset($_COOKIE[$name])) { - $object = json_decode($_COOKIE[$name], false); - setcookie($name, '', time() - 3600, '/'); - return $object; + $cookie = $_COOKIE[$name]; + setcookie($name, '', $this->getCookieOptions(-42000)); + + return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR); } return null; diff --git a/system/src/Grav/Common/Twig/Exception/TwigException.php b/system/src/Grav/Common/Twig/Exception/TwigException.php new file mode 100644 index 0000000000..8f543dcc1e --- /dev/null +++ b/system/src/Grav/Common/Twig/Exception/TwigException.php @@ -0,0 +1,19 @@ + ['all']]), new TwigFilter('array', [$this, 'arrayFilter']), + new TwigFilter('yaml', [$this, 'yamlFilter']), // Object Types new TwigFilter('get_type', [$this, 'getTypeFunc']), @@ -807,6 +808,17 @@ public function arrayFilter($input) return (array)$input; } + /** + * @param array|object $value + * @param int|null $inline + * @param int|null $indent + * @return string + */ + public function yamlFilter($value, $inline = null, $indent = null): string + { + return Yaml::dump($value, $inline, $indent); + } + /** * @param Environment $twig * @return string diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php index 1a32a91a37..3bfe6124e1 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php @@ -43,7 +43,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); $compiler - ->write('throw new \RuntimeException(') + ->write('throw new \Grav\Common\Twig\Exception\TwigException(') ->subcompile($this->getNode('message')) ->write(', ') ->write($this->getAttribute('code') ?: 500) diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 2796c0d458..ef74ce75ab 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -16,6 +16,7 @@ use Grav\Common\Language\LanguageCodes; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\Twig\Exception\TwigException; use Grav\Common\Twig\Extension\FilesystemExtension; use Grav\Common\Twig\Extension\GravExtension; use Grav\Common\Utils; @@ -26,6 +27,7 @@ use Twig\Cache\FilesystemCache; use Twig\Environment; use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; use Twig\Extension\DebugExtension; use Twig\Extension\StringLoaderExtension; @@ -404,38 +406,63 @@ public function processString($string, array $vars = []) */ public function processSite($format = null, array $vars = []) { - // set the page now its been processed - $this->grav->fireEvent('onTwigSiteVariables'); - /** @var Pages $pages */ - $pages = $this->grav['pages']; - /** @var PageInterface $page */ - $page = $this->grav['page']; - $content = $page->content(); + try { + $grav = $this->grav; - $twig_vars = $this->twig_vars; + // set the page now its been processed + $grav->fireEvent('onTwigSiteVariables'); - $twig_vars['theme'] = $this->grav['config']->get('theme'); - $twig_vars['pages'] = $pages->root(); - $twig_vars['page'] = $page; - $twig_vars['header'] = $page->header(); - $twig_vars['media'] = $page->media(); - $twig_vars['content'] = $content; - - // determine if params are set, if so disable twig cache - $params = $this->grav['uri']->params(null, true); - if (!empty($params)) { - $this->twig->setCache(false); - } + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var PageInterface $page */ + $page = $grav['page']; + + $twig_vars = $this->twig_vars; + $twig_vars['theme'] = $grav['config']->get('theme'); + $twig_vars['pages'] = $pages->root(); + $twig_vars['page'] = $page; + $twig_vars['header'] = $page->header(); + $twig_vars['media'] = $page->media(); + $twig_vars['content'] = $page->content(); + + // determine if params are set, if so disable twig cache + $params = $grav['uri']->params(null, true); + if (!empty($params)) { + $this->twig->setCache(false); + } - // Get Twig template layout - $template = $this->getPageTwigTemplate($page, $format); - $page->templateFormat($format); + // Get Twig template layout + $template = $this->getPageTwigTemplate($page, $format); + $page->templateFormat($format); - try { $output = $this->twig->render($template, $vars + $twig_vars); } catch (LoaderError $e) { - $error_msg = $e->getMessage(); - throw new RuntimeException($error_msg, 400, $e); + throw new RuntimeException($e->getMessage(), 400, $e); + } catch (RuntimeError $e) { + $prev = $e->getPrevious(); + if ($prev instanceof TwigException) { + $code = $prev->getCode() ?: 500; + // Fire onPageNotFound event. + $event = new Event([ + 'page' => $page, + 'code' => $code, + 'message' => $prev->getMessage(), + 'exception' => $prev, + 'route' => $grav['route'], + 'request' => $grav['request'] + ]); + $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event); + $newPage = $event['page']; + if ($newPage && $newPage !== $page) { + unset($grav['page']); + $grav['page'] = $newPage; + + return $this->processSite($newPage->templateFormat(), $vars); + } + } + + throw $e; } return $output; diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index c7f71048ed..de325bbe2c 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -1261,7 +1261,7 @@ protected function createFromEnvironment(array $env) $this->port = null; } - if ($this->hasStandardPort()) { + if ($this->port === 0 || $this->hasStandardPort()) { $this->port = null; } @@ -1314,11 +1314,13 @@ protected function createFromString($url) if ($parts === false) { throw new RuntimeException('Malformed URL: ' . $url); } + $port = (int)($parts['port'] ?? 0); + $this->scheme = $parts['scheme'] ?? null; $this->user = $parts['user'] ?? null; $this->password = $parts['pass'] ?? null; $this->host = $parts['host'] ?? null; - $this->port = isset($parts['port']) ? (int)$parts['port'] : null; + $this->port = $port ?: null; $this->path = $parts['path'] ?? ''; $this->query = $parts['query'] ?? ''; $this->fragment = $parts['fragment'] ?? null; diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php index 43fe000810..29f1490f82 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectory.php +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -836,13 +836,22 @@ protected function dynamicFlexField(array &$field, $property, array $call): void $params = (array)$call['params']; $object = $call['object'] ?? null; $method = array_shift($params); + $not = false; + if (str_starts_with($method, '!')) { + $method = substr($method, 1); + $not = true; + } elseif (str_starts_with($method, 'not ')) { + $method = substr($method, 4); + $not = true; + } + $method = trim($method); if ($object && method_exists($object, $method)) { $value = $object->{$method}(...$params); if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { $value = $this->mergeArrays($field[$property], $value); } - $field[$property] = $value; + $field[$property] = $not ? !$value : $value; } } diff --git a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php index e095a5d8f7..dff0836ba8 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php +++ b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php @@ -318,11 +318,11 @@ public function getFileUploadAjaxRoute(): ?Route } /** - * @param string $field - * @param string $filename + * @param string|null $field + * @param string|null $filename * @return Route|null */ - public function getFileDeleteAjaxRoute($field, $filename): ?Route + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route { return null; } diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index 3625189b20..aba0044f77 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -378,22 +378,28 @@ public function getFileUploadAjaxRoute(): ?Route { $object = $this->getObject(); if (!method_exists($object, 'route')) { - return null; + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.upload'); } return $object->route('/edit.json/task:media.upload'); } /** - * @param string $field - * @param string $filename + * @param string|null $field + * @param string|null $filename * @return Route|null */ - public function getFileDeleteAjaxRoute($field, $filename): ?Route + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route { $object = $this->getObject(); if (!method_exists($object, 'route')) { - return null; + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.delete'); } return $object->route('/edit.json/task:media.delete'); diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index fb4e59f36b..945f5adfed 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -71,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** @var array */ private $_meta; /** @var array */ + protected $_original; + /** @var array */ protected $_changes; /** @var string */ protected $storage_key; @@ -370,7 +372,7 @@ public function exists(): bool */ public function searchProperty(string $property, string $search, array $options = null): float { - $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); $value = $this->getProperty($property); return $this->searchValue($property, $value, $search, $options); @@ -384,7 +386,7 @@ public function searchProperty(string $property, string $search, array $options */ public function searchNestedProperty(string $property, string $search, array $options = null): float { - $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); if ($property === 'key') { $value = $this->getKey(); } else { @@ -441,6 +443,16 @@ protected function searchValue(string $name, $value, string $search, array $opti return 0; } + /** + * Get original data before update + * + * @return array + */ + public function getOriginalData(): array + { + return $this->_original ?? []; + } + /** * Get any changes based on data sent to update * @@ -654,7 +666,8 @@ public function update(array $data, array $files = []) } // Store the changes - $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements); + $this->_original = $this->getElements(); + $this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements); } if ($files && method_exists($this, 'setUpdatedMedia')) { diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php index 9539a154cc..32dab19680 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php @@ -38,8 +38,8 @@ public function getFileUploadAjaxRoute(); /** * Get route for deleting files by AJAX. * - * @param string $field Field where the file is associated into. - * @param string $filename Filename for the file. + * @param string|null $field Field where the file is associated into. + * @param string|null $filename Filename for the file. * @return Route|null Returns Route object or null if file uploads are not enabled. */ public function getFileDeleteAjaxRoute($field, $filename); diff --git a/system/src/Grav/Framework/Session/Session.php b/system/src/Grav/Framework/Session/Session.php index ddab08d757..3feae5a2a5 100644 --- a/system/src/Grav/Framework/Session/Session.php +++ b/system/src/Grav/Framework/Session/Session.php @@ -338,23 +338,12 @@ public function invalidate() { $name = $this->getName(); if (null !== $name) { - $params = session_get_cookie_params(); - - $cookie_options = array ( - 'expires' => time() - 42000, - 'path' => $params['path'], - 'domain' => $params['domain'], - 'secure' => $params['secure'], - 'httponly' => $params['httponly'], - 'samesite' => $params['samesite'] - ); - $this->removeCookie(); setcookie( session_name(), '', - $cookie_options + $this->getCookieOptions(-42000) ); } @@ -463,27 +452,36 @@ protected function onSessionStart(): void } /** - * @return void + * Store something in cookie temporarily. + * + * @param int|null $lifetime + * @return array */ - protected function setCookie(): void + public function getCookieOptions(int $lifetime = null): array { $params = session_get_cookie_params(); - $cookie_options = array ( - 'expires' => time() + $params['lifetime'], + return [ + 'expires' => time() + ($lifetime ?? $params['lifetime']), 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'] - ); + ]; + } + /** + * @return void + */ + protected function setCookie(): void + { $this->removeCookie(); setcookie( session_name(), session_id(), - $cookie_options + $this->getCookieOptions() ); } diff --git a/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php b/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php new file mode 100644 index 0000000000..3a41e4a0e2 --- /dev/null +++ b/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php @@ -0,0 +1,70 @@ +getTemplateName(); + $this->blocks[$templateName][] = [ob_get_level(), $blockName]; + } + + public function resolve(\Twig_Template $template, array $context, array $blocks) + { + $templateName = $template->getTemplateName(); + if (empty($this->blocks[$templateName])) { + return; + } + + while ($block = array_pop($this->blocks[$templateName])) { + [$level, $blockName] = $block; + if (ob_get_level() !== $level) { + continue; + } + + $buffer = ob_get_clean(); + + $blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred'); + $template->displayBlock($blockName, $context, $blocks); + + echo $buffer; + } + + if ($parent = $template->getParent($context)) { + $this->resolve($parent, $context, $blocks); + } + } +}