diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b396bd..461acad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,81 @@ +# v1.7.37.1 +## 10/05/2022 + +1. [](#bugfix) + * Fixed a bad return type [#3630](https://github.com/getgrav/grav/issues/3630) + +# v1.7.37 +## 10/05/2022 + +1. [](#new) + * Added new `onPageHeaders()` event to allow for header modification as needed + * Added a `system.pages.dirs` configuration option to allow for configurable paths, and multiple page paths + * Added new `Pages::getSimplePagesHash` which is useful for caching pages specific data + * Updated to latest vendor libraries +1. [](#bugfix) + * An attempt to workaround windows reading locked file issue [getgrav/grav-plugin-admin#2299](https://github.com/getgrav/grav-plugin-admin/issues/2299) + * Force user index file to be updated to fix email addresses [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229) + +# v1.7.36 +## 09/08/2022 + +1. [](#new) + * Added `authorize-*@:` support for Flex blueprints, e.g. `authorize-disabled@: not delete` disables the field if user does not have access to delete object + * Added support for `flex-ignore@` to hide all the nested fields in the blueprint +1. [](#bugfix) + * Fixed login with a capitalised email address when using old users [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229) + +# v1.7.35 +## 08/04/2022 + +1. [](#new) + * Added support for `multipart/form-data` content type in PUT and PATCH requests + * Added support for object relationships + * Added variables `$environment` (string), `$request` (PSR-7 ServerRequestInterface|null) and `$uri` (PSR-7 Uri|null) to be used in `setup.php` +1. [](#improved) + * Minor vendor updates + +# v1.7.34 +## 06/14/2022 + +1. [](#new) + * Added back Yiddish to Language Codes [#3336](https://github.com/getgrav/grav/pull/3336) + * Ignore upcoming `media.json` file in media +1. [](#bugfix) + * Regression: Fixed saving page with a new language causing cache corruption [getgrav/grav-plugin-admin#2282](https://github.com/getgrav/grav-plugin-admin/issues/2282) + * Fixed a potential fatal error when using watermark in images + * Fixed `bin/grav install` command with arbitrary destination folder name + * Fixed Twig `|filter()` allowing code execution + * Fixed login and user search by email not being case-insensitive when using Flex Users + +# v1.7.33 +## 04/25/2022 + +1. [](#improved) + * When saving yaml and markdown, create also a cached version of the file and recompile it in opcache +2. [](#bugfix) + * Fixed missing changes in **yaml** & **markdown** files if saved multiple times during the same second because of a caching issue + * Fixed XSS check not detecting onX events without quotes + * Fixed default collection ordering in pages admin + +# v1.7.32 +## 03/28/2022 + +1. [](#new) + * Added `|replace_last(search, replace)` filter + * Added `parseurl` Twig function to expose PHP's `parse_url` function +2. [](#improved) + * Added multi-language support for page routes in `Utils::url()` + * Set default maximum length for text fields + - `password`: 256 + - `email`: 320 + - `text`, `url`, `hidden`, `commalist`: 2048 + - `text` (multiline), `textarea`: 65536 +3. [](#bugfix) + * Fixed issue with `system.cache.gzip: true` resulted in "Fetch Failed" for PHP 8.0.17 and PHP 8.1.4 [PHP issue #8218](https://github.com/php/php-src/issues/8218) + * Fix for multi-lang issues with Security Report + * Fixed page search not working with selected language [#3316](https://github.com/getgrav/grav/issues/3316) + # v1.7.31 ## 03/14/2022 diff --git a/composer.json b/composer.json index 5855970..cf446da 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "ext-zip": "*", "ext-dom": "*", "ext-libxml": "*", + "ext-gd": "*", "symfony/polyfill-mbstring": "~1.23", "symfony/polyfill-iconv": "^1.23", "symfony/polyfill-php74": "^1.23", @@ -64,7 +65,7 @@ }, "require-dev": { "codeception/codeception": "^4.1", - "phpstan/phpstan": "^1.2", + "phpstan/phpstan": "^1.8", "phpstan/phpstan-deprecation-rules": "^1.0", "phpunit/php-code-coverage": "~9.2", "getgrav/markdowndocs": "^2.0", diff --git a/composer.lock b/composer.lock index d0dc8df..7851fde 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f0530b0fd3e574fef0852376653da5a0", + "content-hash": "3b106a4d1a271673896d67cbdc8623e1", "packages": [ { "name": "composer/ca-bundle", - "version": "1.3.1", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" + "reference": "30897edbfb15e784fe55587b4f73ceefd3c4d98c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", - "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/30897edbfb15e784fe55587b4f73ceefd3c4d98c", + "reference": "30897edbfb15e784fe55587b4f73ceefd3c4d98c", "shasum": "" }, "require": { @@ -64,7 +64,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.1" + "source": "https://github.com/composer/ca-bundle/tree/1.3.3" }, "funding": [ { @@ -80,7 +80,7 @@ "type": "tidelift" } ], - "time": "2021-10-28T20:44:15+00:00" + "time": "2022-07-20T07:14:26+00:00" }, { "name": "composer/semver", @@ -164,16 +164,16 @@ }, { "name": "doctrine/cache", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8" + "reference": "56cd022adb5514472cb144c087393c1821911d09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/4cf401d14df219fa6f38b671f5493449151c9ad8", - "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8", + "url": "https://api.github.com/repos/doctrine/cache/zipball/56cd022adb5514472cb144c087393c1821911d09", + "reference": "56cd022adb5514472cb144c087393c1821911d09", "shasum": "" }, "require": { @@ -185,13 +185,13 @@ "require-dev": { "alcaeus/mongo-php-adapter": "^1.1", "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9", "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", "predis/predis": "~1.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.2 || ^6.0@dev", - "symfony/var-exporter": "^4.4 || ^5.2 || ^6.0@dev" + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" }, "suggest": { "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" @@ -243,7 +243,7 @@ ], "support": { "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/1.12.1" + "source": "https://github.com/doctrine/cache/tree/1.13.0" }, "funding": [ { @@ -259,30 +259,31 @@ "type": "tidelift" } ], - "time": "2021-07-17T14:39:21+00:00" + "time": "2022-05-20T20:06:54+00:00" }, { "name": "doctrine/collections", - "version": "1.6.8", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" + "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", - "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2b44dd4cbca8b5744327de78bafef5945c7e7b5e", + "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e", "shasum": "" }, "require": { + "doctrine/deprecations": "^0.5.3 || ^1", "php": "^7.1.3 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^0.12", + "doctrine/coding-standard": "^9.0 || ^10.0", + "phpstan/phpstan": "^1.4.8", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", - "vimeo/psalm": "^4.2.1" + "vimeo/psalm": "^4.22" }, "type": "library", "autoload": { @@ -326,22 +327,65 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.6.8" + "source": "https://github.com/doctrine/collections/tree/1.8.0" }, - "time": "2021-08-10T18:51:53+00:00" + "time": "2022-09-01T20:12:10+00:00" }, { - "name": "donatj/phpuseragentparser", - "version": "v1.5.0", + "name": "doctrine/deprecations", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/donatj/PhpUserAgent.git", - "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/cc9d872cddfc180c52d084d0dff1e4aad653d37f", - "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" + }, + { + "name": "donatj/phpuseragentparser", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/donatj/PhpUserAgent.git", + "reference": "a35900b93530715f8669c10e49756adde5c8e6fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/a35900b93530715f8669c10e49756adde5c8e6fc", + "reference": "a35900b93530715f8669c10e49756adde5c8e6fc", "shasum": "" }, "require": { @@ -350,6 +394,7 @@ "require-dev": { "camspiers/json-pretty": "~1.0", "donatj/drop": "*", + "ext-json": "*", "phpunit/phpunit": "~4.8|~9" }, "type": "library", @@ -384,11 +429,11 @@ ], "support": { "issues": "https://github.com/donatj/PhpUserAgent/issues", - "source": "https://github.com/donatj/PhpUserAgent/tree/v1.5.0" + "source": "https://github.com/donatj/PhpUserAgent/tree/v1.7.0" }, "funding": [ { - "url": "https://www.paypal.me/donatj/5", + "url": "https://www.paypal.me/donatj/15", "type": "custom" }, { @@ -396,7 +441,7 @@ "type": "github" } ], - "time": "2021-09-16T17:05:03+00:00" + "time": "2022-08-06T15:41:58+00:00" }, { "name": "dragonmantank/cron-expression", @@ -730,16 +775,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.8.3", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85" + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", - "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", "shasum": "" }, "require": { @@ -760,7 +805,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -820,7 +865,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.3" + "source": "https://github.com/guzzle/psr7/tree/1.9.0" }, "funding": [ { @@ -836,20 +881,20 @@ "type": "tidelift" } ], - "time": "2021-10-05T13:56:00+00:00" + "time": "2022-06-20T21:43:03+00:00" }, { "name": "itsgoingd/clockwork", - "version": "v5.1.5", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "6a7b3942224fa53cf3704d9adba636e1f3dfeb9a" + "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/6a7b3942224fa53cf3704d9adba636e1f3dfeb9a", - "reference": "6a7b3942224fa53cf3704d9adba636e1f3dfeb9a", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/74ee05a61296aa7298164ef5346f0a568aa6106e", + "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e", "shasum": "" }, "require": { @@ -896,7 +941,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.5" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.8" }, "funding": [ { @@ -904,31 +949,31 @@ "type": "github" } ], - "time": "2022-02-13T22:57:42+00:00" + "time": "2022-09-25T20:21:14+00:00" }, { "name": "league/climate", - "version": "3.8.1", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/climate.git", - "reference": "22243322c6f049240e0fa6ad6c3873343b6f6055" + "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/climate/zipball/22243322c6f049240e0fa6ad6c3873343b6f6055", - "reference": "22243322c6f049240e0fa6ad6c3873343b6f6055", + "url": "https://api.github.com/repos/thephpleague/climate/zipball/a785a3ac8f584eed4abd45e4e16fe64c46659a28", + "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28", "shasum": "" }, "require": { "php": "^7.3 || ^8.0", - "psr/log": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", "seld/cli-prompt": "^1.0" }, "require-dev": { - "mikey179/vfsstream": "^1.4", + "mikey179/vfsstream": "^1.6.10", "mockery/mockery": "^1.4.2", - "phpunit/phpunit": "^9.5.0" + "phpunit/phpunit": "^9.5.10" }, "suggest": { "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" @@ -967,22 +1012,22 @@ ], "support": { "issues": "https://github.com/thephpleague/climate/issues", - "source": "https://github.com/thephpleague/climate/tree/3.8.1" + "source": "https://github.com/thephpleague/climate/tree/3.8.2" }, - "time": "2022-01-23T14:38:49+00:00" + "time": "2022-06-18T14:42:08+00:00" }, { "name": "matthiasmullie/minify", - "version": "1.3.66", + "version": "1.3.69", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "45fd3b0f1dfa2c965857c6d4a470bea52adc31a6" + "reference": "a61c949cccd086808063611ef9698eabe42ef22f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/45fd3b0f1dfa2c965857c6d4a470bea52adc31a6", - "reference": "45fd3b0f1dfa2c965857c6d4a470bea52adc31a6", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/a61c949cccd086808063611ef9698eabe42ef22f", + "reference": "a61c949cccd086808063611ef9698eabe42ef22f", "shasum": "" }, "require": { @@ -1031,23 +1076,15 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.66" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.69" }, "funding": [ { - "url": "https://github.com/[user1", - "type": "github" - }, - { - "url": "https://github.com/matthiasmullie] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g.", - "type": "github" - }, - { - "url": "https://github.com/user2", + "url": "https://github.com/matthiasmullie", "type": "github" } ], - "time": "2021-01-06T15:18:10+00:00" + "time": "2022-08-01T09:00:18+00:00" }, { "name": "matthiasmullie/path-converter", @@ -1104,16 +1141,16 @@ }, { "name": "maximebf/debugbar", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6" + "reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6", - "reference": "0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/ba0af68dd4316834701ecb30a00ce9604ced3ee9", + "reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9", "shasum": "" }, "require": { @@ -1133,7 +1170,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" } }, "autoload": { @@ -1164,9 +1201,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.18.0" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.18.1" }, - "time": "2021-12-27T18:49:48+00:00" + "time": "2022-03-31T14:55:54+00:00" }, { "name": "miljar/php-exif", @@ -1230,16 +1267,16 @@ }, { "name": "monolog/monolog", - "version": "1.26.1", + "version": "1.27.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5" + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5", - "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", "shasum": "" }, "require": { @@ -1300,7 +1337,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/1.26.1" + "source": "https://github.com/Seldaek/monolog/tree/1.27.1" }, "funding": [ { @@ -1312,7 +1349,7 @@ "type": "tidelift" } ], - "time": "2021-05-28T08:32:12+00:00" + "time": "2022-06-09T08:53:42+00:00" }, { "name": "multiavatar/multiavatar-php", @@ -1363,16 +1400,16 @@ }, { "name": "nyholm/psr7", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "1461e07a0f2a975a52082ca3b769ca912b816226" + "reference": "f734364e38a876a23be4d906a2a089e1315be18a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/1461e07a0f2a975a52082ca3b769ca912b816226", - "reference": "1461e07a0f2a975a52082ca3b769ca912b816226", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", + "reference": "f734364e38a876a23be4d906a2a089e1315be18a", "shasum": "" }, "require": { @@ -1424,7 +1461,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.5.0" + "source": "https://github.com/Nyholm/psr7/tree/1.5.1" }, "funding": [ { @@ -1436,7 +1473,7 @@ "type": "github" } ], - "time": "2022-02-02T18:37:57+00:00" + "time": "2022-06-22T07:13:36+00:00" }, { "name": "nyholm/psr7-server", @@ -2122,23 +2159,23 @@ }, { "name": "rockettheme/toolbox", - "version": "1.6.1", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/rockettheme/toolbox.git", - "reference": "fdf0195ced25b83525d3b084c3e81f05de96ac8c" + "reference": "99448a20a2f78d6480035d72821ecb9e4dc17032" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/fdf0195ced25b83525d3b084c3e81f05de96ac8c", - "reference": "fdf0195ced25b83525d3b084c3e81f05de96ac8c", + "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/99448a20a2f78d6480035d72821ecb9e4dc17032", + "reference": "99448a20a2f78d6480035d72821ecb9e4dc17032", "shasum": "" }, "require": { "ext-json": "*", "php": ">=5.6.0", "pimple/pimple": "^3.0", - "symfony/event-dispatcher": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^3.4|^4.0", "symfony/yaml": "^3.4|^4.0|^5.0" }, "type": "library", @@ -2170,9 +2207,9 @@ ], "support": { "issues": "https://github.com/rockettheme/toolbox/issues", - "source": "https://github.com/rockettheme/toolbox/tree/1.6.1" + "source": "https://github.com/rockettheme/toolbox/tree/1.6.2" }, - "time": "2022-02-08T08:36:03+00:00" + "time": "2022-06-14T16:24:33+00:00" }, { "name": "seld/cli-prompt", @@ -2231,16 +2268,16 @@ }, { "name": "symfony/console", - "version": "v4.4.38", + "version": "v4.4.45", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5a50085bf5460f0c0d60a50b58388c1249826b8a" + "reference": "28b77970939500fb04180166a1f716e75a871ef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5a50085bf5460f0c0d60a50b58388c1249826b8a", - "reference": "5a50085bf5460f0c0d60a50b58388c1249826b8a", + "url": "https://api.github.com/repos/symfony/console/zipball/28b77970939500fb04180166a1f716e75a871ef8", + "reference": "28b77970939500fb04180166a1f716e75a871ef8", "shasum": "" }, "require": { @@ -2301,7 +2338,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.38" + "source": "https://github.com/symfony/console/tree/v4.4.45" }, "funding": [ { @@ -2317,20 +2354,20 @@ "type": "tidelift" } ], - "time": "2022-01-30T21:23:57+00:00" + "time": "2022-08-17T14:50:19+00:00" }, { "name": "symfony/contracts", - "version": "v1.1.11", + "version": "v1.1.13", "source": { "type": "git", "url": "https://github.com/symfony/contracts.git", - "reference": "cf2984f7a99bfae197f9c96f5a16a4041ff6ffb1" + "reference": "9e27f5c175ecbd6fff554d839ff4a432da797168" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/contracts/zipball/cf2984f7a99bfae197f9c96f5a16a4041ff6ffb1", - "reference": "cf2984f7a99bfae197f9c96f5a16a4041ff6ffb1", + "url": "https://api.github.com/repos/symfony/contracts/zipball/9e27f5c175ecbd6fff554d839ff4a432da797168", + "reference": "9e27f5c175ecbd6fff554d839ff4a432da797168", "shasum": "" }, "require": { @@ -2395,7 +2432,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/contracts/tree/v1.1.11" + "source": "https://github.com/symfony/contracts/tree/v1.1.13" }, "funding": [ { @@ -2411,20 +2448,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T13:32:43+00:00" + "time": "2022-06-27T13:16:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.37", + "version": "v4.4.44", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "3ccfcfb96ecce1217d7b0875a0736976bc6e63dc" + "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3ccfcfb96ecce1217d7b0875a0736976bc6e63dc", - "reference": "3ccfcfb96ecce1217d7b0875a0736976bc6e63dc", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1e866e9e5c1b22168e0ce5f0b467f19bba61266a", + "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a", "shasum": "" }, "require": { @@ -2479,7 +2516,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.37" + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.44" }, "funding": [ { @@ -2495,20 +2532,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:41:36+00:00" + "time": "2022-07-20T09:59:04+00:00" }, { "name": "symfony/http-client", - "version": "v4.4.39", + "version": "v4.4.46", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "40342406a975385c5b21e929df46e3fc0278853d" + "reference": "095274039721d26d23225d7b7ba441423b1b2413" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/40342406a975385c5b21e929df46e3fc0278853d", - "reference": "40342406a975385c5b21e929df46e3fc0278853d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/095274039721d26d23225d7b7ba441423b1b2413", + "reference": "095274039721d26d23225d7b7ba441423b1b2413", "shasum": "" }, "require": { @@ -2560,7 +2597,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v4.4.39" + "source": "https://github.com/symfony/http-client/tree/v4.4.46" }, "funding": [ { @@ -2576,20 +2613,20 @@ "type": "tidelift" } ], - "time": "2022-02-28T13:17:32+00:00" + "time": "2022-09-04T17:31:03+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -2604,7 +2641,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2642,7 +2679,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -2658,20 +2695,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "f1aed619e28cb077fc83fac8c4c0383578356e40" + "reference": "143f1881e655bebca1312722af8068de235ae5dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f1aed619e28cb077fc83fac8c4c0383578356e40", - "reference": "f1aed619e28cb077fc83fac8c4c0383578356e40", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/143f1881e655bebca1312722af8068de235ae5dc", + "reference": "143f1881e655bebca1312722af8068de235ae5dc", "shasum": "" }, "require": { @@ -2686,7 +2723,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2725,7 +2762,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.26.0" }, "funding": [ { @@ -2741,20 +2778,20 @@ "type": "tidelift" } ], - "time": "2022-01-04T09:04:05+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -2769,7 +2806,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2808,7 +2845,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -2824,20 +2861,20 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php74", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8" + "reference": "ad4f7d62a17b1187d9f381f0a662aab19ff3c033" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", - "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/ad4f7d62a17b1187d9f381f0a662aab19ff3c033", + "reference": "ad4f7d62a17b1187d9f381f0a662aab19ff3c033", "shasum": "" }, "require": { @@ -2846,7 +2883,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2888,7 +2925,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php74/tree/v1.26.0" }, "funding": [ { @@ -2904,20 +2941,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -2926,7 +2963,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2971,7 +3008,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" }, "funding": [ { @@ -2987,20 +3024,20 @@ "type": "tidelift" } ], - "time": "2022-03-04T08:16:47+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", "shasum": "" }, "require": { @@ -3009,7 +3046,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3050,7 +3087,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" }, "funding": [ { @@ -3066,20 +3103,20 @@ "type": "tidelift" } ], - "time": "2021-09-13T13:58:11+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/process", - "version": "v4.4.37", + "version": "v4.4.44", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "b2d924e5a4cb284f293d5092b1dbf0d364cb8b67" + "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b2d924e5a4cb284f293d5092b1dbf0d364cb8b67", - "reference": "b2d924e5a4cb284f293d5092b1dbf0d364cb8b67", + "url": "https://api.github.com/repos/symfony/process/zipball/5cee9cdc4f7805e2699d9fd66991a0e6df8252a2", + "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2", "shasum": "" }, "require": { @@ -3112,7 +3149,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.37" + "source": "https://github.com/symfony/process/tree/v4.4.44" }, "funding": [ { @@ -3128,20 +3165,20 @@ "type": "tidelift" } ], - "time": "2022-01-27T17:14:04+00:00" + "time": "2022-06-27T13:16:42+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.4.39", + "version": "v4.4.46", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "35237c5e5dcb6593a46a860ba5b29c1d4683d80e" + "reference": "90425fd98d1ecad98e4b2dca9f54f62069193b15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/35237c5e5dcb6593a46a860ba5b29c1d4683d80e", - "reference": "35237c5e5dcb6593a46a860ba5b29c1d4683d80e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/90425fd98d1ecad98e4b2dca9f54f62069193b15", + "reference": "90425fd98d1ecad98e4b2dca9f54f62069193b15", "shasum": "" }, "require": { @@ -3201,7 +3238,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.39" + "source": "https://github.com/symfony/var-dumper/tree/v4.4.46" }, "funding": [ { @@ -3217,20 +3254,20 @@ "type": "tidelift" } ], - "time": "2022-02-25T10:38:15+00:00" + "time": "2022-09-03T23:07:25+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.37", + "version": "v4.4.45", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d7f637cc0f0cc14beb0984f2bb50da560b271311" + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d7f637cc0f0cc14beb0984f2bb50da560b271311", - "reference": "d7f637cc0f0cc14beb0984f2bb50da560b271311", + "url": "https://api.github.com/repos/symfony/yaml/zipball/aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", "shasum": "" }, "require": { @@ -3272,7 +3309,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.37" + "source": "https://github.com/symfony/yaml/tree/v4.4.45" }, "funding": [ { @@ -3288,20 +3325,20 @@ "type": "tidelift" } ], - "time": "2022-01-24T20:11:01+00:00" + "time": "2022-08-02T15:47:23+00:00" }, { "name": "twig/twig", - "version": "v1.44.6", + "version": "v1.44.7", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ae39480f010ef88adc7938503c9b02d3baf2f3b3" + "reference": "0887422319889e442458e48e2f3d9add1a172ad5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae39480f010ef88adc7938503c9b02d3baf2f3b3", - "reference": "ae39480f010ef88adc7938503c9b02d3baf2f3b3", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/0887422319889e442458e48e2f3d9add1a172ad5", + "reference": "0887422319889e442458e48e2f3d9add1a172ad5", "shasum": "" }, "require": { @@ -3354,7 +3391,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v1.44.6" + "source": "https://github.com/twigphp/Twig/tree/v1.44.7" }, "funding": [ { @@ -3366,7 +3403,7 @@ "type": "tidelift" } ], - "time": "2021-11-25T13:31:46+00:00" + "time": "2022-09-28T08:38:36+00:00" }, { "name": "willdurand/negotiation", @@ -3491,16 +3528,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.30", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "a035d77d070fa57fad438e07a65447aeca248c45" + "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/a035d77d070fa57fad438e07a65447aeca248c45", - "reference": "a035d77d070fa57fad438e07a65447aeca248c45", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b88014f3348c93f3df99dc6d0967b0dbfa804474", + "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474", "shasum": "" }, "require": { @@ -3563,11 +3600,11 @@ { "name": "Michael Bodnarchuk", "email": "davert@mail.ua", - "homepage": "http://codegyre.com" + "homepage": "https://codegyre.com" } ], "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "BDD", "TDD", @@ -3577,7 +3614,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.1.30" + "source": "https://github.com/Codeception/Codeception/tree/4.2.2" }, "funding": [ { @@ -3585,7 +3622,7 @@ "type": "open_collective" } ], - "time": "2022-03-05T18:12:30+00:00" + "time": "2022-08-13T13:28:25+00:00" }, { "name": "codeception/lib-asserts", @@ -3760,20 +3797,20 @@ }, { "name": "codeception/module-phpbrowser", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/Codeception/module-phpbrowser.git", - "reference": "770a6be4160a5c0c08d100dd51bff35f6056bbf1" + "reference": "8ba6bede11d0914e74d98691f427fd8f397f192e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/770a6be4160a5c0c08d100dd51bff35f6056bbf1", - "reference": "770a6be4160a5c0c08d100dd51bff35f6056bbf1", + "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/8ba6bede11d0914e74d98691f427fd8f397f192e", + "reference": "8ba6bede11d0914e74d98691f427fd8f397f192e", "shasum": "" }, "require": { - "codeception/codeception": "^4.0", + "codeception/codeception": "^4.1", "codeception/lib-innerbrowser": "^1.3", "guzzlehttp/guzzle": "^6.3|^7.0", "php": ">=5.6.0 <9.0" @@ -3814,22 +3851,22 @@ ], "support": { "issues": "https://github.com/Codeception/module-phpbrowser/issues", - "source": "https://github.com/Codeception/module-phpbrowser/tree/1.0.2" + "source": "https://github.com/Codeception/module-phpbrowser/tree/1.0.3" }, - "time": "2020-10-24T15:29:28+00:00" + "time": "2022-05-21T13:50:41+00:00" }, { "name": "codeception/phpunit-wrapper", - "version": "9.0.7", + "version": "9.0.9", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "7d6b1a5ea4ed28d010e5d36b993db813eb49710b" + "reference": "7439a53ae367986e9c22b2ac00f9d7376bb2f8cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7d6b1a5ea4ed28d010e5d36b993db813eb49710b", - "reference": "7d6b1a5ea4ed28d010e5d36b993db813eb49710b", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7439a53ae367986e9c22b2ac00f9d7376bb2f8cf", + "reference": "7439a53ae367986e9c22b2ac00f9d7376bb2f8cf", "shasum": "" }, "require": { @@ -3863,9 +3900,9 @@ "description": "PHPUnit classes used by Codeception", "support": { "issues": "https://github.com/Codeception/phpunit-wrapper/issues", - "source": "https://github.com/Codeception/phpunit-wrapper/tree/9.0.7" + "source": "https://github.com/Codeception/phpunit-wrapper/tree/9.0.9" }, - "time": "2022-01-26T14:43:10+00:00" + "time": "2022-05-23T06:24:11+00:00" }, { "name": "codeception/stub", @@ -4025,22 +4062,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.4.1", + "version": "7.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79" + "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee0a041b1760e6a53d2a39c8c34115adc2af2c79", - "reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", + "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "guzzlehttp/psr7": "^1.9 || ^2.4", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -4049,10 +4086,10 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -4062,8 +4099,12 @@ }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "7.4-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -4129,7 +4170,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.4.1" + "source": "https://github.com/guzzle/guzzle/tree/7.5.0" }, "funding": [ { @@ -4145,20 +4186,20 @@ "type": "tidelift" } ], - "time": "2021-12-06T18:43:05+00:00" + "time": "2022-08-28T15:39:27+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "b94b2807d85443f9719887892882d0329d1e2598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", + "reference": "b94b2807d85443f9719887892882d0329d1e2598", "shasum": "" }, "require": { @@ -4213,7 +4254,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/1.5.2" }, "funding": [ { @@ -4229,7 +4270,7 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2022-08-28T14:55:35+00:00" }, { "name": "myclabs/deep-copy", @@ -4292,16 +4333,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -4342,9 +4383,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "phar-io/manifest", @@ -4457,249 +4498,22 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" - }, - "time": "2022-01-04T19:58:01+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpstan/phpstan", - "version": "1.4.8", + "version": "1.8.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "2a6d6704b17c4db6190cc3104056c0aad740cb15" + "reference": "536ff0ed719b5679e9f09c17aeaee9b31969cb8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2a6d6704b17c4db6190cc3104056c0aad740cb15", - "reference": "2a6d6704b17c4db6190cc3104056c0aad740cb15", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/536ff0ed719b5679e9f09c17aeaee9b31969cb8e", + "reference": "536ff0ed719b5679e9f09c17aeaee9b31969cb8e", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.2|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -4709,11 +4523,6 @@ "phpstan.phar" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { "files": [ "bootstrap.php" @@ -4724,9 +4533,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.4.8" + "source": "https://github.com/phpstan/phpstan/tree/1.8.7" }, "funding": [ { @@ -4737,16 +4550,12 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-03-04T13:03:56+00:00" + "time": "2022-10-04T14:09:33+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -4800,23 +4609,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -4865,7 +4674,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -4873,7 +4682,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5118,16 +4927,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.18", + "version": "9.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1b5856028273bfd855e60a887278857d872ec67a" + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1b5856028273bfd855e60a887278857d872ec67a", - "reference": "1b5856028273bfd855e60a887278857d872ec67a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", "shasum": "" }, "require": { @@ -5142,7 +4951,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -5150,20 +4958,16 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -5205,7 +5009,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.18" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" }, "funding": [ { @@ -5215,9 +5019,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-03-08T06:52:28+00:00" + "time": "2022-09-25T03:44:45+00:00" }, { "name": "psr/http-client", @@ -5440,16 +5248,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -5502,7 +5310,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -5510,7 +5318,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -5637,16 +5445,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", "shasum": "" }, "require": { @@ -5688,7 +5496,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" }, "funding": [ { @@ -5696,20 +5504,20 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2022-04-03T09:37:03+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -5765,7 +5573,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -5773,7 +5581,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -6128,28 +5936,28 @@ }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -6172,7 +5980,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -6180,7 +5988,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", @@ -6237,16 +6045,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.4.3", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad" + "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/18e73179c6a33d520de1b644941eba108dd811ad", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/081fe28a26b6bd671dea85ef3a4b5003f3c88027", + "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027", "shasum": "" }, "require": { @@ -6289,7 +6097,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.3" + "source": "https://github.com/symfony/browser-kit/tree/v5.4.11" }, "funding": [ { @@ -6305,20 +6113,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-07-27T15:50:05+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.3", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e" + "reference": "c1681789f059ab756001052164726ae88512ae3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b0a190285cd95cb019237851205b8140ef6e368e", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d", + "reference": "c1681789f059ab756001052164726ae88512ae3d", "shasum": "" }, "require": { @@ -6355,7 +6163,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.3" + "source": "https://github.com/symfony/css-selector/tree/v5.4.11" }, "funding": [ { @@ -6371,20 +6179,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -6422,7 +6230,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" }, "funding": [ { @@ -6438,20 +6246,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/dom-crawler", - "version": "v5.4.6", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "c0bda97480d96337bd3866026159a8b358665457" + "reference": "291c1e92281a09152dda089f782e23dedd34bd4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c0bda97480d96337bd3866026159a8b358665457", - "reference": "c0bda97480d96337bd3866026159a8b358665457", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291c1e92281a09152dda089f782e23dedd34bd4f", + "reference": "291c1e92281a09152dda089f782e23dedd34bd4f", "shasum": "" }, "require": { @@ -6497,7 +6305,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.6" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.12" }, "funding": [ { @@ -6513,20 +6321,20 @@ "type": "tidelift" } ], - "time": "2022-03-02T12:42:23+00:00" + "time": "2022-08-03T13:09:21+00:00" }, { "name": "symfony/finder", - "version": "v5.4.3", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d" + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d", + "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", "shasum": "" }, "require": { @@ -6560,7 +6368,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.3" + "source": "https://github.com/symfony/finder/tree/v5.4.11" }, "funding": [ { @@ -6576,7 +6384,7 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:34:36+00:00" + "time": "2022-07-29T07:37:50+00:00" }, { "name": "theseer/tokenizer", @@ -6627,64 +6435,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -6699,11 +6449,12 @@ "ext-curl": "*", "ext-zip": "*", "ext-dom": "*", - "ext-libxml": "*" + "ext-libxml": "*", + "ext-gd": "*" }, "platform-dev": [], "platform-overrides": { "php": "7.3.6" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 787fcd5..09e8083 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -1156,6 +1156,13 @@ form: local6: local6 local7: local7 + log.syslog.tag: + type: text + size: small + label: PLUGIN_ADMIN.SYSLOG_TAG + help: PLUGIN_ADMIN.SYSLOG_TAG_HELP + placeholder: "grav" + debugger: type: tab title: PLUGIN_ADMIN.DEBUGGER diff --git a/system/blueprints/flex/user-accounts.yaml b/system/blueprints/flex/user-accounts.yaml index 8d336f9..74baeb6 100644 --- a/system/blueprints/flex/user-accounts.yaml +++ b/system/blueprints/flex/user-accounts.yaml @@ -125,6 +125,17 @@ config: - username - fullname + relationships: + media: + type: media + cardinality: to-many + avatar: + type: media + cardinality: to-one +# roles: +# type: user-groups +# cardinality: to-many + blueprints: configure: fields: diff --git a/system/config/system.yaml b/system/config/system.yaml index 380d650..2ffd02a 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -35,6 +35,7 @@ home: pages: type: regular # EXPERIMENTAL: Page type: regular or flex + dirs: ['page://'] # Advanced functionality, allows for multiple page paths theme: quark # Default theme (defaults to "quark" theme) order: by: default # Order pages by "default", "alpha" or "date" @@ -144,6 +145,7 @@ log: handler: file # Log handler. Currently supported: file | syslog syslog: facility: local6 # Syslog facilities output + tag: grav # Syslog tag. Default: "grav". debugger: enabled: false # Enable Grav debugger and following settings diff --git a/system/defines.php b/system/defines.php index 5f743db..aa56627 100644 --- a/system/defines.php +++ b/system/defines.php @@ -9,7 +9,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.7.31'); +define('GRAV_VERSION', '1.7.37.1'); 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 f8ce943..b8c121a 100644 --- a/system/src/Grav/Common/Config/Setup.php +++ b/system/src/Grav/Common/Config/Setup.php @@ -182,13 +182,14 @@ class Setup extends Data // If no environment is set, make sure we get one (CLI or hostname). if (null === $environment) { if (defined('GRAV_CLI')) { + $request = null; + $uri = null; $environment = 'cli'; } else { /** @var ServerRequestInterface $request */ $request = $container['request']; - $host = $request->getUri()->getHost(); - - $environment = Utils::substrToString($host, ':'); + $uri = $request->getUri(); + $environment = $uri->getHost(); } } diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index ef6aef5..906fd79 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -515,7 +515,7 @@ class Blueprint extends BlueprintForm $success = $this->resolveActions($user, $actions); } if (!$success) { - $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + static::addPropertyRecursive($field, 'validate', ['ignore' => true]); } } @@ -566,7 +566,7 @@ class Blueprint extends BlueprintForm } if ($matches) { - $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + static::addPropertyRecursive($field, 'validate', ['ignore' => true]); return; } } @@ -577,7 +577,7 @@ class Blueprint extends BlueprintForm * @param mixed $value * @return void */ - protected function addPropertyRecursive(array &$field, $property, $value) + public static function addPropertyRecursive(array &$field, $property, $value) { if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { $field[$property] = array_merge_recursive($field[$property], $value); @@ -587,7 +587,7 @@ class Blueprint extends BlueprintForm if (!empty($field['fields'])) { foreach ($field['fields'] as $key => &$child) { - $this->addPropertyRecursive($child, $property, $value); + static::addPropertyRecursive($child, $property, $value); } } } diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 569ea51..4b2bbf7 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -246,7 +246,9 @@ class Validation return false; } - $max = (int)($params['max'] ?? 0); + $multiline = isset($params['multiline']) && $params['multiline']; + + $max = (int)($params['max'] ?? ($multiline ? 65536 : 2048)); if ($max && $len > $max) { return false; } @@ -256,7 +258,7 @@ class Validation return false; } - if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) { + if (!$multiline && preg_match('/\R/um', $value)) { return false; } @@ -317,6 +319,10 @@ class Validation */ public static function typeCommaList($value, array $params, array $field) { + if (!isset($params['max'])) { + $params['max'] = 2048; + } + return is_array($value) ? true : self::typeText($value, $params, $field); } @@ -379,6 +385,10 @@ class Validation */ public static function typePassword($value, array $params, array $field) { + if (!isset($params['max'])) { + $params['max'] = 256; + } + return self::typeText($value, $params, $field); } @@ -621,6 +631,10 @@ class Validation */ public static function typeEmail($value, array $params, array $field) { + if (!isset($params['max'])) { + $params['max'] = 320; + } + $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value; foreach ($values as $val) { @@ -642,6 +656,10 @@ class Validation */ public static function typeUrl($value, array $params, array $field) { + if (!isset($params['max'])) { + $params['max'] = 2048; + } + return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL); } diff --git a/system/src/Grav/Common/Errors/SystemFacade.php b/system/src/Grav/Common/Errors/SystemFacade.php index edf2b53..e13b0f6 100644 --- a/system/src/Grav/Common/Errors/SystemFacade.php +++ b/system/src/Grav/Common/Errors/SystemFacade.php @@ -43,4 +43,25 @@ class SystemFacade extends \Whoops\Util\SystemFacade $handler(); } } + + + /** + * @param int $httpCode + * + * @return int + */ + public function setHttpResponseCode($httpCode) + { + if (!headers_sent()) { + // Ensure that no 'location' header is present as otherwise this + // will override the HTTP code being set here, and mask the + // expected error page. + header_remove('location'); + + // Work around PHP bug #8218 (8.0.17 & 8.1.4). + header_remove('Content-Encoding'); + } + + return http_response_code($httpCode); + } } diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php index 2228422..f7fb767 100644 --- a/system/src/Grav/Common/File/CompiledFile.php +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -10,6 +10,8 @@ namespace Grav\Common\File; use Exception; +use Grav\Common\Debugger; +use Grav\Common\Grav; use Grav\Common\Utils; use RocketTheme\Toolbox\File\PhpFile; use RuntimeException; @@ -32,9 +34,10 @@ trait CompiledFile public function content($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($this->filename); + $key = md5($filename); $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); $modified = $this->modified(); @@ -48,39 +51,49 @@ trait CompiledFile $class = get_class($this); + $size = filesize($filename); $cache = $file->exists() ? $file->content() : null; // Load real file if cache isn't up to date (or is invalid). if (!isset($cache['@class']) || $cache['@class'] !== $class || $cache['modified'] !== $modified - || $cache['filename'] !== $this->filename + || ($cache['size'] ?? null) !== $size + || $cache['filename'] !== $filename ) { // Attempt to lock the file for writing. try { - $file->lock(false); + $locked = $file->lock(false); } catch (Exception $e) { - // Another process has locked the file; we will check this in a bit. + $locked = false; + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning'); } // Decode RAW file into compiled array. $data = (array)$this->decode($this->raw()); $cache = [ '@class' => $class, - 'filename' => $this->filename, + 'filename' => $filename, 'modified' => $modified, + 'size' => $size, 'data' => $data ]; // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { + if ($locked) { $file->save($cache); $file->unlock(); // Compile cached file into bytecode cache - if (function_exists('opcache_invalidate')) { + if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + $lockName = $file->filename(); + // Silence error if function exists, but is restricted. - @opcache_invalidate($file->filename(), true); + @opcache_invalidate($lockName, true); + @opcache_compile_file($lockName); } } } @@ -89,12 +102,71 @@ trait CompiledFile $this->content = $cache['data']; } } catch (Exception $e) { - throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($this->filename), $e->getMessage()), 500, $e); + throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($filename), $e->getMessage()), 500, $e); } return parent::content($var); } + /** + * Save file. + * + * @param mixed $data Optional data to be saved, usually array. + * @return void + * @throws RuntimeException + */ + public function save($data = null) + { + // Make sure that the cache file is always up to date! + $key = md5($this->filename); + $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); + try { + $locked = $file->lock(); + } catch (Exception $e) { + $locked = false; + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning'); + } + + parent::save($data); + + if ($locked) { + $modified = $this->modified(); + $filename = $this->filename; + $class = get_class($this); + $size = filesize($filename); + + // windows doesn't play nicely with this as it can't read when locked + if (!Utils::isWindows()) { + // Reload data from the filesystem. This ensures that we always cache the correct data (see issue #2282). + $this->raw = $this->content = null; + $data = (array)$this->decode($this->raw()); + } + + // Decode data into compiled array. + $cache = [ + '@class' => $class, + 'filename' => $filename, + 'modified' => $modified, + 'size' => $size, + 'data' => $data + ]; + + $file->save($cache); + $file->unlock(); + + // Compile cached file into bytecode cache + if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + $lockName = $file->filename(); + // Silence error if function exists, but is restricted. + @opcache_invalidate($lockName, true); + @opcache_compile_file($lockName); + } + } + } + /** * Serialize file. * diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 8dac2e5..ad49869 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -31,32 +31,34 @@ abstract class Folder /** * Recursively find the last modified time under given path. * - * @param string $path + * @param array $paths * @return int */ - public static function lastModifiedFolder($path) + public static function lastModifiedFolder(array $paths): int { - if (!file_exists($path)) { - return 0; - } - $last_modified = 0; /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; $flags = RecursiveDirectoryIterator::SKIP_DOTS; - if ($locator->isStream($path)) { - $directory = $locator->getRecursiveIterator($path, $flags); - } else { - $directory = new RecursiveDirectoryIterator($path, $flags); - } - $filter = new RecursiveFolderFilterIterator($directory); - $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST); - foreach ($iterator as $dir) { - $dir_modified = $dir->getMTime(); - if ($dir_modified > $last_modified) { - $last_modified = $dir_modified; + foreach ($paths as $path) { + if (!file_exists($path)) { + return 0; + } + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $filter = new RecursiveFolderFilterIterator($directory); + $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $dir) { + $dir_modified = $dir->getMTime(); + if ($dir_modified > $last_modified) { + $last_modified = $dir_modified; + } } } @@ -66,38 +68,40 @@ abstract class Folder /** * Recursively find the last modified time under given path by file. * - * @param string $path + * @param array $paths * @param string $extensions which files to search for specifically * @return int */ - public static function lastModifiedFile($path, $extensions = 'md|yaml') + public static function lastModifiedFile(array $paths, $extensions = 'md|yaml'): int { - if (!file_exists($path)) { - return 0; - } - $last_modified = 0; /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; $flags = RecursiveDirectoryIterator::SKIP_DOTS; - if ($locator->isStream($path)) { - $directory = $locator->getRecursiveIterator($path, $flags); - } else { - $directory = new RecursiveDirectoryIterator($path, $flags); - } - $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); - $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i'); - /** @var RecursiveDirectoryIterator $file */ - foreach ($iterator as $filepath => $file) { - try { - $file_modified = $file->getMTime(); - if ($file_modified > $last_modified) { - $last_modified = $file_modified; + foreach($paths as $path) { + if (!file_exists($path)) { + return 0; + } + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i'); + + /** @var RecursiveDirectoryIterator $file */ + foreach ($iterator as $file) { + try { + $file_modified = $file->getMTime(); + if ($file_modified > $last_modified) { + $last_modified = $file_modified; + } + } catch (Exception $e) { + Grav::instance()['log']->error('Could not process file: ' . $e->getMessage()); } - } catch (Exception $e) { - Grav::instance()['log']->error('Could not process file: ' . $e->getMessage()); } } @@ -107,28 +111,30 @@ abstract class Folder /** * Recursively md5 hash all files in a path * - * @param string $path + * @param array $paths * @return string */ - public static function hashAllFiles($path) + public static function hashAllFiles(array $paths): string { $files = []; - if (file_exists($path)) { - $flags = RecursiveDirectoryIterator::SKIP_DOTS; + foreach ($paths as $path) { + if (file_exists($path)) { + $flags = RecursiveDirectoryIterator::SKIP_DOTS; - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - if ($locator->isStream($path)) { - $directory = $locator->getRecursiveIterator($path, $flags); - } else { - $directory = new RecursiveDirectoryIterator($path, $flags); - } + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } - $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); - foreach ($iterator as $file) { - $files[] = $file->getPathname() . '?'. $file->getMTime(); + foreach ($iterator as $file) { + $files[] = $file->getPathname() . '?'. $file->getMTime(); + } } } diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index f3978bc..f919d43 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -454,7 +454,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface continue; } - // Get the main key without template and langauge. + // Get the main key without template and language. [$main_key,] = explode('|', $entry['storage_key'] . '|', 2); // Update storage key and language. @@ -527,10 +527,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $language = $options['lang']; $status = 'error'; - $msg = null; $response = []; - $children = null; - $sub_route = null; $extra = null; // Handle leaf_route @@ -610,12 +607,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $children = $page->children(); /** @var PageIndex $children */ $children = $children->getIndex(); - $selectedChildren = $children->filterBy($filters, true); + $selectedChildren = $children->filterBy($filters + ['language' => $language], true); /** @var Header $header */ $header = $page->header(); - if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) { + if (!$field && $header->get('admin.children_display_order', 'collection') === 'collection' && ($orderby = $header->get('content.order.by'))) { // Use custom sorting by page header. $sortby = $orderby; $order = $header->get('content.order.dir', $order); diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php index a05f0eb..6a0afdb 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php @@ -242,6 +242,7 @@ class PageObject extends FlexPageObject { /** @var PageCollection $siblings */ $siblings = $variables['siblings']; + /** @var PageObject $sibling */ foreach ($siblings as $sibling) { $sibling->save(false); } @@ -585,38 +586,46 @@ class PageObject extends FlexPageObject */ public function filterBy(array $filters, bool $recursive = false): bool { + $language = $filters['language'] ?? null; + if (null !== $language) { + /** @var PageObject $test */ + $test = $this->getTranslation($language) ?? $this; + } else { + $test = $this; + } + foreach ($filters as $key => $value) { switch ($key) { case 'search': - $matches = $this->search((string)$value) > 0.0; + $matches = $test->search((string)$value) > 0.0; break; case 'page_type': $types = $value ? explode(',', $value) : []; - $matches = in_array($this->template(), $types, true); + $matches = in_array($test->template(), $types, true); break; case 'extension': - $matches = Utils::contains((string)$value, $this->extension()); + $matches = Utils::contains((string)$value, $test->extension()); break; case 'routable': - $matches = $this->isRoutable() === (bool)$value; + $matches = $test->isRoutable() === (bool)$value; break; case 'published': - $matches = $this->isPublished() === (bool)$value; + $matches = $test->isPublished() === (bool)$value; break; case 'visible': - $matches = $this->isVisible() === (bool)$value; + $matches = $test->isVisible() === (bool)$value; break; case 'module': - $matches = $this->isModule() === (bool)$value; + $matches = $test->isModule() === (bool)$value; break; case 'page': - $matches = $this->isPage() === (bool)$value; + $matches = $test->isPage() === (bool)$value; break; case 'folder': - $matches = $this->isPage() === !$value; + $matches = $test->isPage() === !$value; break; case 'translated': - $matches = $this->hasTranslation() === (bool)$value; + $matches = $test->hasTranslation() === (bool)$value; break; default: $matches = true; diff --git a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php index 4a33b73..868aad7 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php @@ -30,7 +30,7 @@ use function is_string; */ class UserIndex extends FlexIndex implements UserCollectionInterface { - public const VERSION = parent::VERSION . '.1'; + public const VERSION = parent::VERSION . '.2'; /** * @param FlexStorageInterface $storage @@ -50,7 +50,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface // return $index['index']; //} - // Load up to date index. + // Load up-to-date index. $entries = parent::loadEntriesFromStorage($storage); return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]); @@ -142,9 +142,11 @@ class UserIndex extends FlexIndex implements UserCollectionInterface } elseif ($field === 'flex_key') { $user = $this->withKeyField('flex_key')->get($query); } elseif ($field === 'email') { - $user = $this->withKeyField('email')->get($query); + $email = mb_strtolower($query); + $user = $this->withKeyField('email')->get($email); } elseif ($field === 'username') { - $user = $this->get(static::filterUsername($query, $this->getFlexDirectory()->getStorage())); + $username = static::filterUsername($query, $this->getFlexDirectory()->getStorage()); + $user = $this->get($username); } else { $user = $this->__call('find', [$query, $field]); } diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php index 9dec972..f6c9982 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserObject.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -31,6 +31,7 @@ use Grav\Common\Flex\Types\UserGroups\UserGroupIndex; use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\User\Traits\UserTrait; use Grav\Common\Utils; +use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface; use Grav\Framework\File\Formatter\JsonFormatter; use Grav\Framework\File\Formatter\YamlFormatter; use Grav\Framework\Filesystem\Filesystem; @@ -38,7 +39,10 @@ use Grav\Framework\Flex\Flex; use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\Storage\FileStorage; use Grav\Framework\Flex\Traits\FlexMediaTrait; +use Grav\Framework\Flex\Traits\FlexRelationshipsTrait; use Grav\Framework\Form\FormFlashFile; +use Grav\Framework\Media\MediaIdentifier; +use Grav\Framework\Media\UploadedMediaObject; use Psr\Http\Message\UploadedFileInterface; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\File\FileInterface; @@ -77,6 +81,7 @@ class UserObject extends FlexObject implements UserInterface, Countable } use UserTrait; use UserObjectLegacyTrait; + use FlexRelationshipsTrait; /** @var Closure|null */ static public $authorizeCallable; @@ -672,6 +677,81 @@ class UserObject extends FlexObject implements UserInterface, Countable return $folder; } + /** + * @param string $name + * @return array|object|null + * @internal + */ + public function initRelationship(string $name) + { + switch ($name) { + case 'media': + $list = []; + foreach ($this->getMedia()->all() as $filename => $object) { + $list[] = $this->buildMediaObject(null, $filename, $object); + } + + return $list; + case 'avatar': + return $this->buildMediaObject('avatar', basename($this->getAvatarUrl()), $this->getAvatarImage()); + } + + throw new \InvalidArgumentException(sprintf('%s: Relationship %s does not exist', $this->getFlexType(), $name)); + } + + /** + * @return bool Return true if relationships were updated. + */ + protected function updateRelationships(): bool + { + $modified = $this->getRelationships()->getModified(); + if ($modified) { + foreach ($modified as $relationship) { + $name = $relationship->getName(); + switch ($name) { + case 'avatar': + \assert($relationship instanceof ToOneRelationshipInterface); + $this->updateAvatarRelationship($relationship); + break; + default: + throw new \InvalidArgumentException(sprintf('%s: Relationship %s cannot be modified', $this->getFlexType(), $name), 400); + } + } + + $this->resetRelationships(); + + return true; + } + + return false; + } + + /** + * @param ToOneRelationshipInterface $relationship + */ + protected function updateAvatarRelationship(ToOneRelationshipInterface $relationship): void + { + $files = []; + $avatar = $this->getAvatarImage(); + if ($avatar) { + $files['avatar'][$avatar->filename] = null; + } + + $identifier = $relationship->getIdentifier(); + if ($identifier) { + \assert($identifier instanceof MediaIdentifier); + $object = $identifier->getObject(); + if ($object instanceof UploadedMediaObject) { + $uploadedFile = $object->getUploadedFile(); + if ($uploadedFile) { + $files['avatar'][$uploadedFile->getClientFilename()] = $uploadedFile; + } + } + } + + $this->update([], $files); + } + /** * @param string $name * @return Blueprint diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 7031945..4ca84e3 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -12,6 +12,7 @@ namespace Grav\Common\GPM; use Exception; use Grav\Common\Grav; use Grav\Common\Filesystem\Folder; +use Grav\Common\HTTP\Response; use Grav\Common\Inflector; use Grav\Common\Iterator; use Grav\Common\Utils; diff --git a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php index 02e98e7..d8663dc 100644 --- a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php +++ b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php @@ -10,8 +10,8 @@ namespace Grav\Common\GPM\Remote; use Grav\Common\Grav; +use Grav\Common\HTTP\Response; use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection; -use Grav\Common\GPM\Response; use \Doctrine\Common\Cache\FilesystemCache; use RuntimeException; diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index b9b44a4..d2fce73 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -48,6 +48,7 @@ use Grav\Common\Service\TaskServiceProvider; use Grav\Common\Twig\Twig; use Grav\Framework\DI\Container; use Grav\Framework\Psr7\Response; +use Grav\Framework\RequestHandler\Middlewares\MultipartRequestSupport; use Grav\Framework\RequestHandler\RequestHandler; use Grav\Framework\Route\Route; use Grav\Framework\Session\Messages; @@ -117,6 +118,7 @@ class Grav extends Container * @var array All middleware processors that are processed in $this->process() */ protected $middleware = [ + 'multipartRequestSupport', 'initializeProcessor', 'pluginsProcessor', 'themesProcessor', @@ -259,6 +261,9 @@ class Grav extends Container $container = new Container( [ + 'multipartRequestSupport' => function () { + return new MultipartRequestSupport(); + }, 'initializeProcessor' => function () { return new InitializeProcessor($this); }, @@ -341,6 +346,23 @@ class Grav extends Container } } + /** + * Clean any output buffers. Useful when exiting from the application. + * + * Please use $grav->close() and $grav->redirect() instead of calling this one! + * + * @return void + */ + public function cleanOutputBuffers(): void + { + // Make sure nothing extra gets written to the response. + while (ob_get_level()) { + ob_end_clean(); + } + // Work around PHP bug #8218 (8.0.17 & 8.1.4). + header_remove('Content-Encoding'); + } + /** * Terminates Grav request with a response. * @@ -351,10 +373,7 @@ class Grav extends Container */ public function close(ResponseInterface $response): void { - // Make sure nothing extra gets written to the response. - while (ob_get_level()) { - ob_end_clean(); - } + $this->cleanOutputBuffers(); // Close the session. if (isset($this['session'])) { @@ -400,7 +419,7 @@ class Grav extends Container /** * @param ResponseInterface $response * @return never-return - * @deprecated 1.7 Do not use + * @deprecated 1.7 Use $grav->close() instead. */ public function exit(ResponseInterface $response): void { diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php index 3fa4948..ddd37c9 100644 --- a/system/src/Grav/Common/Language/LanguageCodes.php +++ b/system/src/Grav/Common/Language/LanguageCodes.php @@ -153,6 +153,8 @@ class LanguageCodes 'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ], 'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ], 'xh' => [ 'name' => 'Xhosa', 'nativeName' => 'isiXhosa' ], + 'yi' => [ 'name' => 'Yiddish', 'nativeName' => 'ייִדיש', 'orientation' => 'rtl' ], + 'ydd' => [ 'name' => 'Yiddish', 'nativeName' => 'ייִדיש', 'orientation' => 'rtl' ], 'zh' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], 'zh-CN' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], 'zh-TW' => [ 'name' => 'Chinese (Traditional)', 'nativeName' => '正體中文 (繁體)' ], diff --git a/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php index 9334759..912a7c4 100644 --- a/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php +++ b/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php @@ -158,14 +158,6 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj */ public function thumbnail($type = 'auto'); - /** - * Return URL to file. - * - * @param bool $reset - * @return string - */ - public function url($reset = true); - /** * Turn the current Medium into a Link * @@ -221,18 +213,6 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj #[\ReturnTypeWillChange] public function __call($method, $args); - /** - * Get value by using dot notation for nested arrays/objects. - * - * @example $value = $this->get('this.is.my.nested.variable'); - * - * @param string $name Dot separated path to the requested value. - * @param mixed $default Default value (or null). - * @param string|null $separator Separator, defaults to '.' - * @return mixed Value. - */ - public function get($name, $default = null, $separator = null); - /** * Set value by using dot notation for nested arrays/objects. * diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php index 6c0a6c3..b80fd96 100644 --- a/system/src/Grav/Common/Page/Media.php +++ b/system/src/Grav/Common/Page/Media.php @@ -152,7 +152,7 @@ class Media extends AbstractMedia foreach ($iterator as $file => $info) { // Ignore folders and Markdown files. $filename = $info->getFilename(); - if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) { + if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || $filename === 'media.json' || strpos($filename, '.') === 0) { continue; } diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 42d8f0e..ab6ba4a 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -361,8 +361,8 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate // Scaling operations $scale = ($scale ?? $config->get('system.images.watermark.scale', 100)) / 100; - $wwidth = $this->get('width') * $scale; - $wheight = $this->get('height') * $scale; + $wwidth = (int)$this->get('width') * $scale; + $wheight = (int)$this->get('height') * $scale; $watermark->resize($wwidth, $wheight); // Position operations @@ -377,11 +377,11 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate break; case 'bottom': - $positionY = $this->get('height')-$wheight; + $positionY = (int)$this->get('height')-$wheight; break; case 'center': - $positionY = ($this->get('height')/2) - ($wheight/2); + $positionY = ((int)$this->get('height')/2) - ($wheight/2); break; } @@ -392,11 +392,11 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate break; case 'right': - $positionX = $this->get('width')-$wwidth; + $positionX = (int)$this->get('width')-$wwidth; break; case 'center': - $positionX = ($this->get('width')/2) - ($wwidth/2); + $positionX = ((int)$this->get('width')/2) - ($wwidth/2); break; } diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 25f8ff6..ccf676f 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -622,7 +622,12 @@ class Page implements PageInterface $headers['Vary'] = 'Accept-Encoding'; } - return $headers; + + // Added new Headers event + $headers_obj = (object) $headers; + Grav::instance()->fireEvent('onPageHeaders', new Event(['headers' => $headers_obj])); + + return (array)$headers_obj; } /** diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index ec48b51..6e2d14f 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -88,6 +88,8 @@ class Pages /** @var string */ protected $check_method; /** @var string */ + protected $simple_pages_hash; + /** @var string */ protected $pages_cache_id; /** @var bool */ protected $initialized = false; @@ -100,6 +102,7 @@ class Pages /** @var string|null */ protected static $home_route; + /** * Constructor * @@ -1712,10 +1715,7 @@ class Pages /** @var Language $language */ $language = $this->grav['language']; - $pages_dir = $locator->findResource('page://'); - if (!is_string($pages_dir)) { - throw new RuntimeException('Internal Error'); - } + $pages_dirs = $this->getPagesPaths(); // Set active language $this->active_lang = $language->getActive(); @@ -1731,16 +1731,17 @@ class Pages $hash = 0; break; case 'folder': - $hash = Folder::lastModifiedFolder($pages_dir); + $hash = Folder::lastModifiedFolder($pages_dirs); break; case 'hash': - $hash = Folder::hashAllFiles($pages_dir); + $hash = Folder::hashAllFiles($pages_dirs); break; default: - $hash = Folder::lastModifiedFile($pages_dir); + $hash = Folder::lastModifiedFile($pages_dirs); } - $this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum()); + $this->simple_pages_hash = json_encode($pages_dirs) . $hash . $config->checksum(); + $this->pages_cache_id = md5($this->simple_pages_hash . $language->getActive()); /** @var Cache $cache */ $cache = $this->grav['cache']; @@ -1760,18 +1761,39 @@ class Pages $this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..'); } - $this->resetPages($pages_dir); + $this->resetPages($pages_dirs); + } + + protected function getPagesPaths(): array + { + $grav = Grav::instance(); + $locator = $grav['locator']; + $paths = []; + + $dirs = (array) $grav['config']->get('system.pages.dirs', ['page://']); + foreach ($dirs as $dir) { + $path = $locator->findResource($dir); + if (file_exists($path)) { + $paths[] = $path; + } + } + + return $paths; } /** * Accessible method to manually reset the pages cache * - * @param string $pages_dir + * @param array $pages_dirs */ - public function resetPages($pages_dir): void + public function resetPages(array $pages_dirs): void { $this->sort = []; - $this->recurse($pages_dir); + + foreach ($pages_dirs as $dir) { + $this->recurse($dir); + } + $this->buildRoutes(); // cache if needed @@ -1795,7 +1817,7 @@ class Pages * @throws RuntimeException * @internal */ - protected function recurse($directory, PageInterface $parent = null) + protected function recurse(string $directory, PageInterface $parent = null) { $directory = rtrim($directory, DS); $page = new Page; @@ -2177,7 +2199,7 @@ class Pages * @param array $list * @return array */ - protected function arrayShuffle($list) + protected function arrayShuffle(array $list): array { $keys = array_keys($list); shuffle($keys); @@ -2193,7 +2215,7 @@ class Pages /** * @return string */ - protected function getVersion() + protected function getVersion(): string { return $this->directory ? 'flex' : 'regular'; } @@ -2204,10 +2226,20 @@ class Pages * this is particularly useful to know if pages have changed and you want * to sync another cache with pages cache - works best in `onPagesInitialized()` * - * @return string + * @return null|string */ - public function getPagesCacheId() + public function getPagesCacheId(): ?string { return $this->pages_cache_id; } + + /** + * Get the simple pages hash that is not md5 encoded, and isn't specific to language + * + * @return null|string + */ + public function getSimplePagesHash(): ?string + { + return $this->simple_pages_hash; + } } diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php index 04e06ed..24f4cf9 100644 --- a/system/src/Grav/Common/Processors/InitializeProcessor.php +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -251,7 +251,8 @@ class InitializeProcessor extends ProcessorBase $log->popHandler(); $facility = $config->get('system.log.syslog.facility', 'local6'); - $logHandler = new SyslogHandler('grav', $facility); + $tag = $config->get('system.log.syslog.tag', 'grav'); + $logHandler = new SyslogHandler($tag, $facility); $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); $logHandler->setFormatter($formatter); diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php index 779e619..9826185 100644 --- a/system/src/Grav/Common/Security.php +++ b/system/src/Grav/Common/Security.php @@ -97,7 +97,7 @@ class Security */ public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null) { - $routes = $pages->routes(); + $routes = $pages->getList(null, 0, true); // Remove duplicate for homepage unset($routes['/']); @@ -110,26 +110,23 @@ class Security 'steps' => count($routes), ]); - foreach ($routes as $path) { + foreach (array_keys($routes) as $route) { $status && $status([ 'type' => 'progress', ]); try { - $page = $pages->get($path); + $page = $pages->find($route); + if ($page->exists()) { + // call the content to load/cache it + $header = (array) $page->header(); + $content = $page->value('content'); - // call the content to load/cache it - $header = (array) $page->header(); - $content = $page->value('content'); + $data = ['header' => $header, 'content' => $content]; + $results = static::detectXssFromArray($data); - $data = ['header' => $header, 'content' => $content]; - $results = static::detectXssFromArray($data); - - if (!empty($results)) { - if ($route) { - $list[$page->route()] = $results; - } else { - $list[$page->filePathClean()] = $results; + if (!empty($results)) { + $list[$page->rawRoute()] = $results; } } } catch (Exception $e) { @@ -222,7 +219,8 @@ class Security $string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8'); // Strip whitespace characters - $string = preg_replace('!\s!u', '', $string); + $string = preg_replace('!\s!u', ' ', $string); + $stripped = preg_replace('!\s!u', '', $string); // Set the patterns we'll test against $patterns = [ @@ -245,7 +243,7 @@ class Security // Iterate over rules and return label if fail foreach ($patterns as $name => $regex) { if (!empty($enabled_rules[$name])) { - if (preg_match($regex, $string) || preg_match($regex, $orig)) { + if (preg_match($regex, $string) || preg_match($regex, $stripped) || preg_match($regex, $orig)) { return $name; } } diff --git a/system/src/Grav/Common/Service/FlexServiceProvider.php b/system/src/Grav/Common/Service/FlexServiceProvider.php index e7b283a..2119292 100644 --- a/system/src/Grav/Common/Service/FlexServiceProvider.php +++ b/system/src/Grav/Common/Service/FlexServiceProvider.php @@ -97,7 +97,9 @@ class FlexServiceProvider implements ServiceProviderInterface 'options' => [ 'file' => 'user', 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}', - 'key' => 'storage_key' + 'key' => 'storage_key', + 'indexed' => true, + 'case_sensitive' => false ], ]; } @@ -107,7 +109,9 @@ class FlexServiceProvider implements ServiceProviderInterface 'class' => UserFileStorage::class, 'options' => [ 'pattern' => '{FOLDER}/{KEY}{EXT}', - 'key' => 'username' + 'key' => 'username', + 'indexed' => true, + 'case_sensitive' => false ], ]; } diff --git a/system/src/Grav/Common/Twig/Extension/GravExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php index 52e2a18..64fb315 100644 --- a/system/src/Grav/Common/Twig/Extension/GravExtension.php +++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php @@ -9,6 +9,7 @@ namespace Grav\Common\Twig\Extension; +use CallbackFilterIterator; use Cron\CronExpression; use Grav\Common\Config\Config; use Grav\Common\Data\Data; @@ -41,6 +42,7 @@ use JsonSerializable; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use Traversable; use Twig\Environment; +use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\Extension\GlobalsInterface; use Twig\Loader\FilesystemLoader; @@ -145,6 +147,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']), new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']), new TwigFilter('nicecron', [$this, 'niceCronFilter']), + new TwigFilter('replace_last', [$this, 'replaceLastFilter']), // Translations new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]), @@ -166,6 +169,9 @@ class GravExtension extends AbstractExtension implements GlobalsInterface // PHP methods new TwigFilter('count', 'count'), new TwigFilter('array_diff', 'array_diff'), + + // Security fix + new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]), ]; } @@ -194,6 +200,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface new TwigFunction('gist', [$this, 'gistFunc']), new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']), new TwigFunction('pathinfo', 'pathinfo'), + new TwigFunction('parseurl', 'parse_url'), new TwigFunction('random_string', [$this, 'randomStringFunc']), new TwigFunction('repeat', [$this, 'repeatFunc']), new TwigFunction('regex_replace', [$this, 'regexReplace']), @@ -547,6 +554,21 @@ class GravExtension extends AbstractExtension implements GlobalsInterface return $cron->getText('en'); } + /** + * @param string|mixed $str + * @param string $search + * @param string $replace + * @return string|mixed + */ + public function replaceLastFilter($str, $search, $replace) + { + if (is_string($str) && ($pos = mb_strrpos($str, $search)) !== false) { + $str = mb_substr($str, 0, $pos) . $replace . mb_substr($str, $pos + mb_strlen($search)); + } + + return $str; + } + /** * Get Cron object for a crontab 'at' format * @@ -1659,4 +1681,20 @@ class GravExtension extends AbstractExtension implements GlobalsInterface return is_string($var); } } + + /** + * @param Environment $env + * @param array $array + * @param callable|string $arrow + * @return array|CallbackFilterIterator + * @throws RuntimeError + */ + function filterFilter(Environment $env, $array, $arrow) + { + if (is_string($arrow) && Utils::isDangerousFunction($arrow)) { + throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.'); + } + + return twig_array_filter($env, $array, $arrow); + } } diff --git a/system/src/Grav/Common/User/DataUser/UserCollection.php b/system/src/Grav/Common/User/DataUser/UserCollection.php index d0ada09..c997bdc 100644 --- a/system/src/Grav/Common/User/DataUser/UserCollection.php +++ b/system/src/Grav/Common/User/DataUser/UserCollection.php @@ -112,11 +112,12 @@ class UserCollection implements UserCollectionInterface // If not found, try the fields if (!$user->exists()) { + $query = mb_strtolower($query); foreach ($files as $file) { if (Utils::endsWith($file, YAML_EXT)) { $find_user = $this->load(trim(Utils::pathinfo($file, PATHINFO_FILENAME))); foreach ($fields as $field) { - if (isset($find_user[$field]) && $find_user[$field] === $query) { + if (isset($find_user[$field]) && mb_strtolower($find_user[$field]) === $query) { return $find_user; } } diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 145a893..55e7f1d 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -83,6 +83,7 @@ abstract class Utils $resource = false; if (static::contains((string)$input, '://')) { + // Url contains a scheme (https:// , user:// etc). /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; @@ -134,6 +135,16 @@ abstract class Utils $resource = $locator->findResource($input, false); } } else { + // Just a path. + /** @var Pages $pages */ + $pages = $grav['pages']; + + // Is this a page? + $page = $pages->find($input, true); + if ($page && $page->routable()) { + return $page->url($domain); + } + $root = preg_quote($uri->rootUrl(), '#'); $pattern = '#(' . $root . '$|' . $root . '/)#'; if (!empty($root) && preg_match($pattern, $input, $matches)) { @@ -657,18 +668,17 @@ abstract class Utils */ public static function download($file, $force_download = true, $sec = 0, $bytes = 1024, array $options = []) { + $grav = Grav::instance(); + if (file_exists($file)) { // fire download event - Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file, 'options' => &$options])); + $grav->fireEvent('onBeforeDownload', new Event(['file' => $file, 'options' => &$options])); $file_parts = static::pathinfo($file); $mimetype = $options['mime'] ?? static::getMimeByExtension($file_parts['extension']); $size = filesize($file); // File size - // clean all buffers - while (ob_get_level()) { - ob_end_clean(); - } + $grav->cleanOutputBuffers(); // required for IE, otherwise Content-Disposition may be ignored if (ini_get('zlib.output_compression')) { @@ -703,8 +713,8 @@ abstract class Utils $new_length = $size; header('Content-Length: ' . $size); - if (Grav::instance()['config']->get('system.cache.enabled')) { - $expires = $options['expires'] ?? Grav::instance()['config']->get('system.pages.expires'); + if ($grav['config']->get('system.cache.enabled')) { + $expires = $options['expires'] ?? $grav['config']->get('system.pages.expires'); if ($expires > 0) { $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires); header('Cache-Control: max-age=' . $expires); diff --git a/system/src/Grav/Console/Cli/InstallCommand.php b/system/src/Grav/Console/Cli/InstallCommand.php index 33198cb..ae38253 100644 --- a/system/src/Grav/Console/Cli/InstallCommand.php +++ b/system/src/Grav/Console/Cli/InstallCommand.php @@ -147,7 +147,7 @@ class InstallCommand extends GravCommand foreach ($this->config['git'] as $repo => $data) { $path = $this->destination . DS . $data['path']; if (!file_exists($path)) { - exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return); + exec('cd ' . escapeshellarg($this->destination) . ' && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return); if (!$return) { $io->writeln('SUCCESS cloned ' . $data['url'] . ' -> ' . $path . ''); diff --git a/system/src/Grav/Console/ConsoleTrait.php b/system/src/Grav/Console/ConsoleTrait.php index 0af4075..d35fc7c 100644 --- a/system/src/Grav/Console/ConsoleTrait.php +++ b/system/src/Grav/Console/ConsoleTrait.php @@ -288,7 +288,7 @@ trait ConsoleTrait { $composer = Composer::getComposerExecutor(); - return system($composer . ' --working-dir="'.$path.'" --no-interaction --no-dev --prefer-dist -o '. $action); + return system($composer . ' --working-dir=' . escapeshellarg($path) . ' --no-interaction --no-dev --prefer-dist -o '. $action); } /** diff --git a/system/src/Grav/Console/Gpm/DirectInstallCommand.php b/system/src/Grav/Console/Gpm/DirectInstallCommand.php index fbb0220..881d6f9 100644 --- a/system/src/Grav/Console/Gpm/DirectInstallCommand.php +++ b/system/src/Grav/Console/Gpm/DirectInstallCommand.php @@ -10,12 +10,11 @@ namespace Grav\Console\Gpm; use Exception; -use Grav\Common\Cache; use Grav\Common\Grav; use Grav\Common\Filesystem\Folder; +use Grav\Common\HTTP\Response; use Grav\Common\GPM\GPM; use Grav\Common\GPM\Installer; -use Grav\Common\GPM\Response; use Grav\Console\GpmCommand; use RuntimeException; use Symfony\Component\Console\Input\InputArgument; diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php index d25ebe3..5ad47f8 100644 --- a/system/src/Grav/Console/Gpm/InstallCommand.php +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -11,10 +11,10 @@ namespace Grav\Console\Gpm; use Exception; use Grav\Common\Filesystem\Folder; +use Grav\Common\HTTP\Response; use Grav\Common\GPM\GPM; use Grav\Common\GPM\Installer; use Grav\Common\GPM\Licenses; -use Grav\Common\GPM\Response; use Grav\Common\GPM\Remote\Package; use Grav\Common\Grav; use Grav\Common\Utils; @@ -485,7 +485,7 @@ class InstallCommand extends GpmCommand { $io = $this->getIO(); - exec('cd ' . $this->destination); + exec('cd ' . escapeshellarg($this->destination)); $to = $this->destination . DS . $package->install_path; $from = $this->getSymlinkSource($package); diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index a3d53ff..30bc5f5 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -11,8 +11,8 @@ namespace Grav\Console\Gpm; use Exception; use Grav\Common\Filesystem\Folder; +use Grav\Common\HTTP\Response; use Grav\Common\GPM\Installer; -use Grav\Common\GPM\Response; use Grav\Common\GPM\Upgrader; use Grav\Common\Grav; use Grav\Console\GpmCommand; diff --git a/system/src/Grav/Framework/Cache/CacheTrait.php b/system/src/Grav/Framework/Cache/CacheTrait.php index 37dd6c9..46248fe 100644 --- a/system/src/Grav/Framework/Cache/CacheTrait.php +++ b/system/src/Grav/Framework/Cache/CacheTrait.php @@ -157,9 +157,6 @@ trait CacheTrait $this->validateKeys($keys); $keys = array_unique($keys); $keys = array_combine($keys, $keys); - if (empty($keys)) { - return []; - } $list = $this->doGetMultiple($keys, $this->miss); diff --git a/system/src/Grav/Framework/Contracts/Media/MediaObjectInterface.php b/system/src/Grav/Framework/Contracts/Media/MediaObjectInterface.php new file mode 100644 index 0000000..75b80f0 --- /dev/null +++ b/system/src/Grav/Framework/Contracts/Media/MediaObjectInterface.php @@ -0,0 +1,52 @@ +|ArrayAccess + * @phpstan-pure + */ + public function getIdentifierMeta(); +} diff --git a/system/src/Grav/Framework/Contracts/Relationships/RelationshipInterface.php b/system/src/Grav/Framework/Contracts/Relationships/RelationshipInterface.php new file mode 100644 index 0000000..c0a7edf --- /dev/null +++ b/system/src/Grav/Framework/Contracts/Relationships/RelationshipInterface.php @@ -0,0 +1,81 @@ + + */ +interface RelationshipInterface extends Countable, IteratorAggregate, JsonSerializable, Serializable +{ + /** + * @return string + * @phpstan-pure + */ + public function getName(): string; + + /** + * @return string + * @phpstan-pure + */ + public function getType(): string; + + /** + * @return bool + * @phpstan-pure + */ + public function isModified(): bool; + + /** + * @return string + * @phpstan-pure + */ + public function getCardinality(): string; + + /** + * @return P + * @phpstan-pure + */ + public function getParent(): IdentifierInterface; + + /** + * @param string $id + * @param string|null $type + * @return bool + * @phpstan-pure + */ + public function has(string $id, string $type = null): bool; + + /** + * @param T $identifier + * @return bool + * @phpstan-pure + */ + public function hasIdentifier(IdentifierInterface $identifier): bool; + + /** + * @param T $identifier + * @return bool + */ + public function addIdentifier(IdentifierInterface $identifier): bool; + + /** + * @param T|null $identifier + * @return bool + */ + public function removeIdentifier(IdentifierInterface $identifier = null): bool; + + /** + * @return iterable + */ + public function getIterator(): iterable; +} diff --git a/system/src/Grav/Framework/Contracts/Relationships/RelationshipsInterface.php b/system/src/Grav/Framework/Contracts/Relationships/RelationshipsInterface.php new file mode 100644 index 0000000..4bd90a3 --- /dev/null +++ b/system/src/Grav/Framework/Contracts/Relationships/RelationshipsInterface.php @@ -0,0 +1,53 @@ +> + * @extends Iterator> + */ +interface RelationshipsInterface extends Countable, ArrayAccess, Iterator, JsonSerializable +{ + /** + * @return bool + * @phpstan-pure + */ + public function isModified(): bool; + + /** + * @return array + */ + public function getModified(): array; + + /** + * @return int + * @phpstan-pure + */ + public function count(): int; + + /** + * @param string $offset + * @return RelationshipInterface|null + */ + public function offsetGet($offset): ?RelationshipInterface; + + /** + * @return RelationshipInterface|null + */ + public function current(): ?RelationshipInterface; + + /** + * @return string + * @phpstan-pure + */ + public function key(): string; +} diff --git a/system/src/Grav/Framework/Contracts/Relationships/ToManyRelationshipInterface.php b/system/src/Grav/Framework/Contracts/Relationships/ToManyRelationshipInterface.php new file mode 100644 index 0000000..723bef6 --- /dev/null +++ b/system/src/Grav/Framework/Contracts/Relationships/ToManyRelationshipInterface.php @@ -0,0 +1,55 @@ + + */ +interface ToManyRelationshipInterface extends RelationshipInterface +{ + /** + * @param positive-int $pos + * @return IdentifierInterface|null + */ + public function getNthIdentifier(int $pos): ?IdentifierInterface; + + /** + * @param string $id + * @param string|null $type + * @return T|null + * @phpstan-pure + */ + public function getIdentifier(string $id, string $type = null): ?IdentifierInterface; + + /** + * @param string $id + * @param string|null $type + * @return T|null + * @phpstan-pure + */ + public function getObject(string $id, string $type = null): ?object; + + /** + * @param iterable $identifiers + * @return bool + */ + public function addIdentifiers(iterable $identifiers): bool; + + /** + * @param iterable $identifiers + * @return bool + */ + public function replaceIdentifiers(iterable $identifiers): bool; + + /** + * @param iterable $identifiers + * @return bool + */ + public function removeIdentifiers(iterable $identifiers): bool; +} diff --git a/system/src/Grav/Framework/Contracts/Relationships/ToOneRelationshipInterface.php b/system/src/Grav/Framework/Contracts/Relationships/ToOneRelationshipInterface.php new file mode 100644 index 0000000..0e6aeb9 --- /dev/null +++ b/system/src/Grav/Framework/Contracts/Relationships/ToOneRelationshipInterface.php @@ -0,0 +1,37 @@ + + */ +interface ToOneRelationshipInterface extends RelationshipInterface +{ + /** + * @param string|null $id + * @param string|null $type + * @return T|null + * @phpstan-pure + */ + public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface; + + /** + * @param string|null $id + * @param string|null $type + * @return T|null + * @phpstan-pure + */ + public function getObject(string $id = null, string $type = null): ?object; + + /** + * @param T|null $identifier + * @return bool + */ + public function replaceIdentifier(IdentifierInterface $identifier = null): bool; +} diff --git a/system/src/Grav/Framework/Flex/FlexCollection.php b/system/src/Grav/Framework/Flex/FlexCollection.php index 186c60a..9c5b16c 100644 --- a/system/src/Grav/Framework/Flex/FlexCollection.php +++ b/system/src/Grav/Framework/Flex/FlexCollection.php @@ -147,6 +147,10 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface */ public function search(string $search, $properties = null, array $options = null) { + $directory = $this->getFlexDirectory(); + $properties = $directory->getSearchProperties($properties); + $options = $directory->getSearchOptions($options); + $matching = $this->call('search', [$search, $properties, $options]); $matching = array_filter($matching); diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php index f96633b..7197284 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectory.php +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -170,6 +170,44 @@ class FlexDirectory implements FlexDirectoryInterface return null === $name ? $this->config : $this->config->get($name, $default); } + /** + * @param string|string[]|null $properties + * @return array + */ + public function getSearchProperties($properties = null): array + { + if (null !== $properties) { + return (array)$properties; + } + + $properties = $this->getConfig('data.search.fields'); + if (!$properties) { + $fields = $this->getConfig('admin.views.list.fields') ?? $this->getConfig('admin.list.fields', []); + foreach ($fields as $property => $value) { + if (!empty($value['link'])) { + $properties[] = $property; + } + } + } + + return $properties; + } + + /** + * @param array|null $options + * @return array + */ + public function getSearchOptions(array $options = null): array + { + if (empty($options['merge'])) { + return $options ?? (array)$this->getConfig('data.search.options'); + } + + unset($options['merge']); + + return $options + (array)$this->getConfig('data.search.options'); + } + /** * @param string|null $name * @param array $options @@ -750,6 +788,7 @@ class FlexDirectory implements FlexDirectoryInterface public function reloadIndex(): void { $this->getCache('index')->clear(); + $this->getIndex()::loadEntriesFromStorage($this->getStorage()); $this->indexes = []; $this->objects = []; @@ -797,6 +836,9 @@ class FlexDirectory implements FlexDirectoryInterface $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) { $this->dynamicFlexField($field, $property, $call); }); + $blueprint->addDynamicHandler('authorize', function (array &$field, $property, array &$call) { + $this->dynamicAuthorizeField($field, $property, $call); + }); if ($context) { $blueprint->setContext($context); @@ -878,6 +920,40 @@ class FlexDirectory implements FlexDirectoryInterface if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { $value = $this->mergeArrays($field[$property], $value); } + $value = $not ? !$value : $value; + + if ($property === 'ignore' && $value) { + Blueprint::addPropertyRecursive($field, 'validate', ['ignore' => true]); + } else { + $field[$property] = $value; + } + } + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicAuthorizeField(array &$field, $property, array $call): void + { + $params = (array)$call['params']; + $object = $call['object'] ?? null; + $permission = array_shift($params); + $not = false; + if (str_starts_with($permission, '!')) { + $permission = substr($permission, 1); + $not = true; + } elseif (str_starts_with($permission, 'not ')) { + $permission = substr($permission, 4); + $not = true; + } + $permission = trim($permission); + + if ($object) { + $value = $object->isAuthorized($permission) ?? false; + $field[$property] = $not ? !$value : $value; } } diff --git a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php index 709f418..1e715a5 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php +++ b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php @@ -275,6 +275,7 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable 'unique_id' => $this->getUniqueId(), 'form_name' => $this->getName(), 'folder' => $this->getFlashFolder(), + 'id' => $this->getFlashId(), 'directory' => $this->getDirectory() ]; diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index a1263f6..55759df 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -326,6 +326,7 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable 'unique_id' => $this->getUniqueId(), 'form_name' => $this->getName(), 'folder' => $this->getFlashFolder(), + 'id' => $this->getFlashId(), 'object' => $this->getObject() ]; diff --git a/system/src/Grav/Framework/Flex/FlexIdentifier.php b/system/src/Grav/Framework/Flex/FlexIdentifier.php new file mode 100644 index 0000000..ec47ed8 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexIdentifier.php @@ -0,0 +1,75 @@ + + */ +class FlexIdentifier extends Identifier +{ + /** @var string */ + private $keyField; + /** @var FlexObjectInterface|null */ + private $object = null; + + /** + * @param FlexObjectInterface $object + * @return FlexIdentifier + */ + public static function createFromObject(FlexObjectInterface $object): FlexIdentifier + { + $instance = new static($object->getKey(), $object->getFlexType(), 'key'); + $instance->setObject($object); + + return $instance; + } + + /** + * IdentifierInterface constructor. + * @param string $id + * @param string $type + * @param string $keyField + */ + public function __construct(string $id, string $type, string $keyField = 'key') + { + parent::__construct($id, $type); + + $this->keyField = $keyField; + } + + /** + * @return T + */ + public function getObject(): ?FlexObjectInterface + { + if (!isset($this->object)) { + /** @var Flex $flex */ + $flex = Grav::instance()['flex']; + + $this->object = $flex->getObject($this->getId(), $this->getType(), $this->keyField); + } + + return $this->object; + } + + /** + * @param T $object + */ + public function setObject(FlexObjectInterface $object): void + { + $type = $this->getType(); + if ($type !== $object->getFlexType()) { + throw new RuntimeException(sprintf('Object has to be type %s, %s given', $type, $object->getFlexType())); + } + + $this->object = $object; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php index 54438af..d02065f 100644 --- a/system/src/Grav/Framework/Flex/FlexIndex.php +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -162,6 +162,10 @@ class FlexIndex extends ObjectIndex implements FlexIndexInterface */ public function search(string $search, $properties = null, array $options = null) { + $directory = $this->getFlexDirectory(); + $properties = $directory->getSearchProperties($properties); + $options = $directory->getSearchOptions($options); + return $this->__call('search', [$search, $properties, $options]); } diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index afc656c..3808b2c 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -287,17 +287,9 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function search(string $search, $properties = null, array $options = null): float { - $properties = (array)($properties ?? $this->getFlexDirectory()->getConfig('data.search.fields')); - if (!$properties) { - $fields = $this->getFlexDirectory()->getConfig('admin.views.list.fields') ?? $this->getFlexDirectory()->getConfig('admin.list.fields', []); - foreach ($fields as $property => $value) { - if (!empty($value['link'])) { - $properties[] = $property; - } - } - } - - $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); + $directory = $this->getFlexDirectory(); + $properties = $directory->getSearchProperties($properties); + $options = $directory->getSearchOptions($options); $weight = 0; foreach ($properties as $property) { diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php index ad437eb..0f55595 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -22,6 +22,9 @@ use Grav\Framework\Cache\CacheInterface; use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Form\FormFlashFile; +use Grav\Framework\Media\Interfaces\MediaObjectInterface; +use Grav\Framework\Media\MediaObject; +use Grav\Framework\Media\UploadedMediaObject; use Psr\Http\Message\UploadedFileInterface; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RuntimeException; @@ -30,7 +33,6 @@ use function in_array; use function is_array; use function is_callable; use function is_int; -use function is_object; use function is_string; use function strpos; @@ -297,6 +299,55 @@ trait FlexMediaTrait ]; } + /** + * @param string|null $field + * @param string $filename + * @param MediaObjectInterface|null $image + * @return MediaObject|UploadedMediaObject + */ + protected function buildMediaObject(?string $field, string $filename, MediaObjectInterface $image = null) + { + if (!$image) { + $media = $field ? $this->getMediaField($field) : null; + if ($media) { + $image = $media[$filename]; + } + } + + return new MediaObject($field, $filename, $image, $this); + } + + /** + * @param string|null $field + * @return array + */ + protected function buildMediaList(?string $field): array + { + $names = $field ? (array)$this->getNestedProperty($field) : []; + $media = $field ? $this->getMediaField($field) : null; + if (null === $media) { + $media = $this->getMedia(); + } + + $list = []; + foreach ($names as $key => $val) { + $name = is_string($val) ? $val : $key; + $medium = $media[$name]; + if ($medium) { + if ($medium->uploaded_file) { + $upload = $medium->uploaded_file; + $id = $upload instanceof FormFlashFile ? $upload->getId() : "{$field}-{$name}"; + + $list[] = new UploadedMediaObject($id, $field, $name, $upload); + } else { + $list[] = $this->buildMediaObject($field, $name, $medium); + } + } + } + + return $list; + } + /** * @param array $files * @return void @@ -375,7 +426,7 @@ trait FlexMediaTrait $updated = false; foreach ($this->getUpdatedMedia() as $filename => $upload) { if (is_array($upload)) { - // Uses new format with [UploadedFileInterface, array]. + /** @var array{UploadedFileInterface,array} $upload */ $settings = $upload[1]; if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) { $upload = $upload[0]; @@ -388,6 +439,7 @@ trait FlexMediaTrait $updated = true; if ($medium) { $medium->uploaded = true; + $medium->uploaded_file = $upload; $media->add($filename, $medium); } elseif (is_callable([$media, 'hide'])) { $media->hide($filename); diff --git a/system/src/Grav/Framework/Flex/Traits/FlexRelationshipsTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexRelationshipsTrait.php new file mode 100644 index 0000000..2a73eba --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexRelationshipsTrait.php @@ -0,0 +1,61 @@ +_relationships)) { + $blueprint = $this->getBlueprint(); + $options = $blueprint->get('config/relationships', []); + $parent = FlexIdentifier::createFromObject($this); + + $this->_relationships = new Relationships($parent, $options); + } + + return $this->_relationships; + } + + /** + * @param string $name + * @return RelationshipInterface|null + */ + public function getRelationship(string $name): ?RelationshipInterface + { + return $this->getRelationships()[$name]; + } + + protected function resetRelationships(): void + { + $this->_relationships = null; + } + + /** + * @param iterable $collection + * @return array + */ + protected function buildFlexIdentifierList(iterable $collection): array + { + $list = []; + foreach ($collection as $object) { + $list[] = FlexIdentifier::createFromObject($object); + } + + return $list; + } +} diff --git a/system/src/Grav/Framework/Form/FormFlash.php b/system/src/Grav/Framework/Form/FormFlash.php index 863eb4a..207015b 100644 --- a/system/src/Grav/Framework/Form/FormFlash.php +++ b/system/src/Grav/Framework/Form/FormFlash.php @@ -31,6 +31,8 @@ class FormFlash implements FormFlashInterface /** @var bool */ protected $exists; /** @var string */ + protected $id; + /** @var string */ protected $sessionId; /** @var string */ protected $uniqueId; @@ -75,9 +77,12 @@ class FormFlash implements FormFlashInterface }); } - $this->sessionId = $config['session_id'] ?? 'no-session'; + $this->id = $config['id'] ?? ''; + $this->sessionId = $config['session_id'] ?? ''; $this->uniqueId = $config['unique_id'] ?? ''; + $this->setUser($config['user'] ?? null); + $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : ''); /** @var UniformResourceLocator $locator */ @@ -133,6 +138,14 @@ class FormFlash implements FormFlashInterface return $data; } + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->id && $this->uniqueId ? $this->id . '/' . $this->uniqueId : ''; + } + /** * @inheritDoc */ @@ -390,8 +403,8 @@ class FormFlash implements FormFlashInterface */ public function clearFiles() { - foreach ($this->files as $field => $files) { - foreach ($files as $name => $upload) { + foreach ($this->files as $files) { + foreach ($files as $upload) { $this->removeTmpFile($upload['tmp_name'] ?? ''); } } @@ -406,6 +419,7 @@ class FormFlash implements FormFlashInterface { return [ 'form' => $this->formName, + 'id' => $this->getId(), 'unique_id' => $this->uniqueId, 'url' => $this->url, 'user' => $this->user, diff --git a/system/src/Grav/Framework/Form/FormFlashFile.php b/system/src/Grav/Framework/Form/FormFlashFile.php index 65af544..54047d1 100644 --- a/system/src/Grav/Framework/Form/FormFlashFile.php +++ b/system/src/Grav/Framework/Form/FormFlashFile.php @@ -28,6 +28,8 @@ use function sprintf; */ class FormFlashFile implements UploadedFileInterface, JsonSerializable { + /** @var string */ + private $id; /** @var string */ private $field; /** @var bool */ @@ -45,6 +47,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable */ public function __construct(string $field, array $upload, FormFlash $flash) { + $this->id = $flash->getId() ?: $flash->getUniqueId(); $this->field = $field; $this->upload = $upload; $this->flash = $flash; @@ -107,6 +110,11 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable } } + public function getId(): string + { + return $this->id; + } + /** * @return string */ @@ -222,6 +230,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable public function __debugInfo() { return [ + 'id:private' => $this->id, 'field:private' => $this->field, 'moved:private' => $this->moved, 'upload:private' => $this->upload, diff --git a/system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php b/system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php index 44bee02..093006a 100644 --- a/system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php +++ b/system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php @@ -22,6 +22,13 @@ interface FormFlashInterface extends \JsonSerializable */ public function __construct($config); + /** + * Get unique form flash id if set. + * + * @return string + */ + public function getId(): string; + /** * Get session Id associated to this form instance. * diff --git a/system/src/Grav/Framework/Form/Traits/FormTrait.php b/system/src/Grav/Framework/Form/Traits/FormTrait.php index 710d8b0..a69ad1e 100644 --- a/system/src/Grav/Framework/Form/Traits/FormTrait.php +++ b/system/src/Grav/Framework/Form/Traits/FormTrait.php @@ -453,10 +453,10 @@ trait FormTrait 'session_id' => $this->getSessionId(), 'unique_id' => $this->getUniqueId(), 'form_name' => $this->getName(), - 'folder' => $this->getFlashFolder() + 'folder' => $this->getFlashFolder(), + 'id' => $this->getFlashId() ]; - $this->flash = new FormFlash($config); $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null); } @@ -486,7 +486,8 @@ trait FormTrait 'session_id' => $this->getSessionId(), 'unique_id' => $uniqueId, 'form_name' => $name, - 'folder' => $this->getFlashFolder() + 'folder' => $this->getFlashFolder(), + 'id' => $this->getFlashId() ]; $flash = new FormFlash($config); if ($flash->exists() && $flash->getFormName() === $name) { @@ -614,6 +615,28 @@ trait FormTrait return strpos($path, '!!') === false ? rtrim($path, '/') : null; } + /** + * @return string|null + */ + protected function getFlashId(): ?string + { + // Fill template token keys/value pairs. + $dataMap = [ + '[FORM_NAME]' => $this->getName(), + '[SESSIONID]' => 'session', + '[USERNAME]' => '!!', + '[USERNAME_OR_SESSIONID]' => '!!', + '[ACCOUNT]' => 'account' + ]; + + $flashLookupFolder = $this->getFlashLookupFolder(); + + $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder); + + // Make sure we only return valid paths. + return strpos($path, '!!') === false ? rtrim($path, '/') : null; + } + /** * @return string */ diff --git a/system/src/Grav/Framework/Media/Interfaces/MediaObjectInterface.php b/system/src/Grav/Framework/Media/Interfaces/MediaObjectInterface.php index f4409c5..39a063f 100644 --- a/system/src/Grav/Framework/Media/Interfaces/MediaObjectInterface.php +++ b/system/src/Grav/Framework/Media/Interfaces/MediaObjectInterface.php @@ -9,9 +9,39 @@ namespace Grav\Framework\Media\Interfaces; +use Psr\Http\Message\UploadedFileInterface; + /** * Class implements media object interface. + * + * @property UploadedFileInterface|null $uploaded_file */ interface MediaObjectInterface { + /** + * Returns an array containing the file metadata + * + * @return array + */ + public function getMeta(); + + /** + * Return URL to file. + * + * @param bool $reset + * @return string + */ + public function url($reset = true); + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $this->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null); } diff --git a/system/src/Grav/Framework/Media/MediaIdentifier.php b/system/src/Grav/Framework/Media/MediaIdentifier.php new file mode 100644 index 0000000..986e997 --- /dev/null +++ b/system/src/Grav/Framework/Media/MediaIdentifier.php @@ -0,0 +1,150 @@ + + */ +class MediaIdentifier extends Identifier +{ + /** @var MediaObjectInterface|null */ + private $object = null; + + /** + * @param MediaObjectInterface $object + * @return MediaIdentifier + */ + public static function createFromObject(MediaObjectInterface $object): MediaIdentifier + { + $instance = new static($object->getId()); + $instance->setObject($object); + + return $instance; + } + + /** + * @param string $id + */ + public function __construct(string $id) + { + parent::__construct($id, 'media'); + } + + /** + * @return T + */ + public function getObject(): ?MediaObjectInterface + { + if (!isset($this->object)) { + $type = $this->getType(); + $id = $this->getId(); + + $parts = explode('/', $id); + if ($type === 'media' && str_starts_with($id, 'uploads/')) { + array_shift($parts); + [, $folder, $uniqueId, $field, $filename] = $this->findFlash($parts); + + $flash = $this->getFlash($folder, $uniqueId); + if ($flash->exists()) { + + $uploadedFile = $flash->getFilesByField($field)[$filename] ?? null; + + $this->object = UploadedMediaObject::createFromFlash($flash, $field, $filename, $uploadedFile); + } + } else { + $type = array_shift($parts); + $key = array_shift($parts); + $field = array_shift($parts); + $filename = implode('/', $parts); + + $flexObject = $this->getFlexObject($type, $key); + if ($flexObject && method_exists($flexObject, 'getMediaField') && method_exists($flexObject, 'getMedia')) { + $media = $field !== 'media' ? $flexObject->getMediaField($field) : $flexObject->getMedia(); + $image = null; + if ($media) { + $image = $media[$filename]; + } + + $this->object = new MediaObject($field, $filename, $image, $flexObject); + } + } + + if (!isset($this->object)) { + throw new \RuntimeException(sprintf('Object not found for identifier {type: "%s", id: "%s"}', $type, $id)); + } + } + + return $this->object; + } + + /** + * @param T $object + */ + public function setObject(MediaObjectInterface $object): void + { + $type = $this->getType(); + $objectType = $object->getType(); + + if ($type !== $objectType) { + throw new \RuntimeException(sprintf('Object has to be type %s, %s given', $type, $objectType)); + } + + $this->object = $object; + } + + protected function findFlash(array $parts): ?array + { + $type = array_shift($parts); + if ($type === 'account') { + /** @var UserInterface|null $user */ + $user = Grav::instance()['user'] ?? null; + $folder = $user->getMediaFolder(); + } else { + $folder = 'tmp://'; + } + + if (!$folder) { + return null; + } + + do { + $part = array_shift($parts); + $folder .= "/{$part}"; + } while (!str_starts_with($part, 'flex-')); + + $uniqueId = array_shift($parts); + $field = array_shift($parts); + $filename = implode('/', $parts); + + return [$type, $folder, $uniqueId, $field, $filename]; + } + + protected function getFlash(string $folder, string $uniqueId): FlexFormFlash + { + $config = [ + 'unique_id' => $uniqueId, + 'folder' => $folder + ]; + + return new FlexFormFlash($config); + } + + protected function getFlexObject(string $type, string $key): ?FlexObjectInterface + { + /** @var Flex $flex */ + $flex = Grav::instance()['flex']; + + return $flex->getObject($key, $type); + } +} diff --git a/system/src/Grav/Framework/Media/MediaObject.php b/system/src/Grav/Framework/Media/MediaObject.php new file mode 100644 index 0000000..8a438bf --- /dev/null +++ b/system/src/Grav/Framework/Media/MediaObject.php @@ -0,0 +1,215 @@ +field = $field; + $this->filename = $filename; + $this->media = $media; + $this->object = $object; + } + + /** + * @return string + */ + public function getType(): string + { + return 'media'; + } + + /** + * @return string + */ + public function getId(): string + { + $field = $this->field; + $object = $this->object; + $path = $field ? "/{$field}/" : '/media/'; + + return $object->getType() . '/' . $object->getKey() . $path . basename($this->filename); + } + + /** + * @return bool + */ + public function exists(): bool + { + return $this->media !== null; + } + + /** + * @return array + */ + public function getMeta(): array + { + if (!isset($this->media)) { + return []; + } + + return $this->media->getMeta(); + } + + /** + * @param string $field + * @return mixed|null + */ + public function get(string $field) + { + if (!isset($this->media)) { + return null; + } + + return $this->media->get($field); + } + + /** + * @return string + */ + public function getUrl(): string + { + if (!isset($this->media)) { + return ''; + } + + return $this->media->url(); + } + + /** + * Create media response. + * + * @param array $actions + * @return Response + */ + public function createResponse(array $actions): ResponseInterface + { + if (!isset($this->media)) { + return $this->create404Response($actions); + } + + $media = $this->media; + + if ($actions) { + $media = $this->processMediaActions($media, $actions); + } + + // FIXME: This only works for images + if (!$media instanceof ImageMedium) { + throw new \RuntimeException('Not Implemented', 500); + } + + $filename = $media->path(false); + $time = filemtime($filename); + $size = filesize($filename); + $body = fopen($filename, 'rb'); + $headers = [ + 'Content-Type' => $media->get('mime'), + 'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT', + 'ETag' => sprintf('%x-%x', $size, $time) + ]; + + return new Response(200, $headers, $body); + } + + /** + * Process media actions + * + * @param GravMediaObjectInterface $medium + * @param array $actions + * @return GravMediaObjectInterface + */ + protected function processMediaActions(GravMediaObjectInterface $medium, array $actions): GravMediaObjectInterface + { + // loop through actions for the image and call them + foreach ($actions as $method => $params) { + $matches = []; + + if (preg_match('/\[(.*)]/', $params, $matches)) { + $args = [explode(',', $matches[1])]; + } else { + $args = explode(',', $params); + } + + try { + $medium->{$method}(...$args); + } catch (Throwable $e) { + // Ignore all errors for now and just skip the action. + } + } + + return $medium; + } + + /** + * @param array $actions + * @return Response + */ + protected function create404Response(array $actions): Response + { + // Display placeholder image. + $filename = static::$placeholderImage; + + $time = filemtime($filename); + $size = filesize($filename); + $body = fopen($filename, 'rb'); + $headers = [ + 'Content-Type' => 'image/svg', + 'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT', + 'ETag' => sprintf('%x-%x', $size, $time) + ]; + + return new Response(404, $headers, $body); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'type' => $this->getType(), + 'id' => $this->getId() + ]; + } + + /** + * @return string[] + */ + public function __debugInfo(): array + { + return $this->jsonSerialize(); + } +} diff --git a/system/src/Grav/Framework/Media/UploadedMediaObject.php b/system/src/Grav/Framework/Media/UploadedMediaObject.php new file mode 100644 index 0000000..0fe12e1 --- /dev/null +++ b/system/src/Grav/Framework/Media/UploadedMediaObject.php @@ -0,0 +1,172 @@ +getId(); + + return new static($id, $field, $filename, $uploadedFile); + } + + /** + * @param string $id + * @param string|null $field + * @param string $filename + * @param UploadedFileInterface|null $uploadedFile + */ + public function __construct(string $id, ?string $field, string $filename, ?UploadedFileInterface $uploadedFile = null) + { + $this->id = $id; + $this->field = $field; + $this->filename = $filename; + $this->uploadedFile = $uploadedFile; + if ($uploadedFile) { + $this->meta = [ + 'filename' => $uploadedFile->getClientFilename(), + 'mime' => $uploadedFile->getClientMediaType(), + 'size' => $uploadedFile->getSize() + ]; + } else { + $this->meta = []; + } + } + + /** + * @return string + */ + public function getType(): string + { + return 'media'; + } + + /** + * @return string + */ + public function getId(): string + { + $id = $this->id; + $field = $this->field; + $path = $field ? "/{$field}/" : ''; + + return 'uploads/' . $id . $path . basename($this->filename); + } + + /** + * @return bool + */ + public function exists(): bool + { + //return $this->uploadedFile !== null; + return false; + } + + /** + * @return array + */ + public function getMeta(): array + { + return $this->meta; + } + + /** + * @param string $field + * @return mixed|null + */ + public function get(string $field) + { + return $this->meta[$field] ?? null; + } + + /** + * @return string + */ + public function getUrl(): string + { + return ''; + } + + /** + * @return UploadedFileInterface|null + */ + public function getUploadedFile(): ?UploadedFileInterface + { + return $this->uploadedFile; + } + + /** + * @param array $actions + * @return Response + */ + public function createResponse(array $actions): ResponseInterface + { + // Display placeholder image. + $filename = static::$placeholderImage; + + $time = filemtime($filename); + $size = filesize($filename); + $body = fopen($filename, 'rb'); + $headers = [ + 'Content-Type' => 'image/svg', + 'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT', + 'ETag' => sprintf('%x-%x', $size, $time) + ]; + + return new Response(404, $headers, $body); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'type' => $this->getType(), + 'id' => $this->getId() + ]; + } + + /** + * @return string[] + */ + public function __debugInfo(): array + { + return $this->jsonSerialize(); + } +} diff --git a/system/src/Grav/Framework/Object/Identifiers/Identifier.php b/system/src/Grav/Framework/Object/Identifiers/Identifier.php new file mode 100644 index 0000000..69f41d2 --- /dev/null +++ b/system/src/Grav/Framework/Object/Identifiers/Identifier.php @@ -0,0 +1,66 @@ +id = $id; + $this->type = $type; + } + + /** + * @return string + * @phpstan-pure + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return string + * @phpstan-pure + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'type' => $this->type, + 'id' => $this->id + ]; + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return $this->jsonSerialize(); + } +} diff --git a/system/src/Grav/Framework/Relationships/Relationships.php b/system/src/Grav/Framework/Relationships/Relationships.php new file mode 100644 index 0000000..6485682 --- /dev/null +++ b/system/src/Grav/Framework/Relationships/Relationships.php @@ -0,0 +1,217 @@ + + */ +class Relationships implements RelationshipsInterface +{ + /** @var P */ + protected $parent; + /** @var array */ + protected $options; + + /** @var RelationshipInterface[] */ + protected $relationships; + + /** + * Relationships constructor. + * @param P $parent + * @param array $options + */ + public function __construct(IdentifierInterface $parent, array $options) + { + $this->parent = $parent; + $this->options = $options; + $this->relationships = []; + } + + /** + * @return bool + * @phpstan-pure + */ + public function isModified(): bool + { + return !empty($this->getModified()); + } + + /** + * @return RelationshipInterface[] + * @phpstan-pure + */ + public function getModified(): array + { + $list = []; + foreach ($this->relationships as $name => $relationship) { + if ($relationship->isModified()) { + $list[$name] = $relationship; + } + } + + return $list; + } + + /** + * @return int + * @phpstan-pure + */ + public function count(): int + { + return count($this->options); + } + + /** + * @param string $offset + * @return bool + * @phpstan-pure + */ + public function offsetExists($offset): bool + { + return isset($this->options[$offset]); + } + + /** + * @param string $offset + * @return RelationshipInterface|null + */ + public function offsetGet($offset): ?RelationshipInterface + { + if (!isset($this->relationships[$offset])) { + $options = $this->options[$offset] ?? null; + if (null === $options) { + return null; + } + + $this->relationships[$offset] = $this->createRelationship($offset, $options); + } + + return $this->relationships[$offset]; + } + + /** + * @param string $offset + * @param mixed $value + * @return never-return + */ + public function offsetSet($offset, $value) + { + throw new RuntimeException('Setting relationship is not supported', 500); + } + + /** + * @param string $offset + * @return never-return + */ + public function offsetUnset($offset) + { + throw new RuntimeException('Removing relationship is not allowed', 500); + } + + /** + * @return RelationshipInterface|null + */ + public function current(): ?RelationshipInterface + { + $name = key($this->options); + if ($name === null) { + return null; + } + + return $this->offsetGet($name); + } + + /** + * @return string + * @phpstan-pure + */ + public function key(): string + { + return key($this->options); + } + + /** + * @return void + * @phpstan-pure + */ + public function next(): void + { + next($this->options); + } + + /** + * @return void + * @phpstan-pure + */ + public function rewind(): void + { + reset($this->options); + } + + /** + * @return bool + * @phpstan-pure + */ + public function valid(): bool + { + return key($this->options) !== null; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $list = []; + foreach ($this as $name => $relationship) { + $list[$name] = $relationship->jsonSerialize(); + } + + return $list; + } + + /** + * @param string $name + * @param array $options + * @return ToOneRelationship|ToManyRelationship + */ + private function createRelationship(string $name, array $options): RelationshipInterface + { + $data = null; + + $parent = $this->parent; + if ($parent instanceof FlexIdentifier) { + $object = $parent->getObject(); + if (!method_exists($object, 'initRelationship')) { + throw new RuntimeException(sprintf('Bad relationship %s', $name), 500); + } + + $data = $object->initRelationship($name); + } + + $cardinality = $options['cardinality'] ?? ''; + switch ($cardinality) { + case 'to-one': + $relationship = new ToOneRelationship($parent, $name, $options, $data); + break; + case 'to-many': + $relationship = new ToManyRelationship($parent, $name, $options, $data ?? []); + break; + default: + throw new RuntimeException(sprintf('Bad relationship cardinality %s', $cardinality), 500); + } + + return $relationship; + } +} diff --git a/system/src/Grav/Framework/Relationships/ToManyRelationship.php b/system/src/Grav/Framework/Relationships/ToManyRelationship.php new file mode 100644 index 0000000..3ea501b --- /dev/null +++ b/system/src/Grav/Framework/Relationships/ToManyRelationship.php @@ -0,0 +1,259 @@ + + */ +class ToManyRelationship implements ToManyRelationshipInterface +{ + /** @template-use RelationshipTrait */ + use RelationshipTrait; + use Serializable; + + /** @var IdentifierInterface[] */ + protected $identifiers = []; + + /** + * ToManyRelationship constructor. + * @param string $name + * @param IdentifierInterface $parent + * @param iterable $identifiers + */ + public function __construct(IdentifierInterface $parent, string $name, array $options, iterable $identifiers = []) + { + $this->parent = $parent; + $this->name = $name; + + $this->parseOptions($options); + $this->addIdentifiers($identifiers); + + $this->modified = false; + } + + /** + * @return string + * @phpstan-pure + */ + public function getCardinality(): string + { + return 'to-many'; + } + + /** + * @return int + * @phpstan-pure + */ + public function count(): int + { + return count($this->identifiers); + } + + /** + * @return array + */ + public function fetch(): array + { + $list = []; + foreach ($this->identifiers as $identifier) { + if (is_callable([$identifier, 'getObject'])) { + $identifier = $identifier->getObject(); + } + $list[] = $identifier; + } + + return $list; + } + + /** + * @param string $id + * @param string|null $type + * @return bool + * @phpstan-pure + */ + public function has(string $id, string $type = null): bool + { + return $this->getIdentifier($id, $type) !== null; + } + + /** + * @param positive-int $pos + * @return IdentifierInterface|null + */ + public function getNthIdentifier(int $pos): ?IdentifierInterface + { + $items = array_keys($this->identifiers); + $key = $items[$pos - 1] ?? null; + if (null === $key) { + return null; + } + + return $this->identifiers[$key] ?? null; + } + + /** + * @param string $id + * @param string|null $type + * @return IdentifierInterface|null + * @phpstan-pure + */ + public function getIdentifier(string $id, string $type = null): ?IdentifierInterface + { + if (null === $type) { + $type = $this->getType(); + } + + if ($type === 'media' && !str_contains($id, '/')) { + $name = $this->name; + $id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id; + } + + $key = "{$type}/{$id}"; + + return $this->identifiers[$key] ?? null; + } + + /** + * @param string $id + * @param string|null $type + * @return T|null + */ + public function getObject(string $id, string $type = null): ?object + { + $identifier = $this->getIdentifier($id, $type); + if ($identifier && is_callable([$identifier, 'getObject'])) { + $identifier = $identifier->getObject(); + } + + return $identifier; + } + + /** + * @param IdentifierInterface $identifier + * @return bool + */ + public function addIdentifier(IdentifierInterface $identifier): bool + { + return $this->addIdentifiers([$identifier]); + } + + /** + * @param IdentifierInterface|null $identifier + * @return bool + */ + public function removeIdentifier(IdentifierInterface $identifier = null): bool + { + return !$identifier || $this->removeIdentifiers([$identifier]); + } + + /** + * @param iterable $identifiers + * @return bool + */ + public function addIdentifiers(iterable $identifiers): bool + { + foreach ($identifiers as $identifier) { + $type = $identifier->getType(); + $id = $identifier->getId(); + $key = "{$type}/{$id}"; + + $this->identifiers[$key] = $this->checkIdentifier($identifier); + $this->modified = true; + } + + return true; + } + + /** + * @param iterable $identifiers + * @return bool + */ + public function replaceIdentifiers(iterable $identifiers): bool + { + $this->identifiers = []; + $this->modified = true; + + return $this->addIdentifiers($identifiers); + } + + /** + * @param iterable $identifiers + * @return bool + */ + public function removeIdentifiers(iterable $identifiers): bool + { + foreach ($identifiers as $identifier) { + $type = $identifier->getType(); + $id = $identifier->getId(); + $key = "{$type}/{$id}"; + + unset($this->identifiers[$key]); + $this->modified = true; + } + + return true; + } + + /** + * @return iterable + * @phpstan-pure + */ + public function getIterator(): iterable + { + return new ArrayIterator($this->identifiers); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $list = []; + foreach ($this->getIterator() as $item) { + $list[] = $item->jsonSerialize(); + } + + return $list; + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'parent' => $this->parent, + 'name' => $this->name, + 'type' => $this->type, + 'options' => $this->options, + 'modified' => $this->modified, + 'identifiers' => $this->identifiers, + ]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->parent = $data['parent']; + $this->name = $data['name']; + $this->type = $data['type']; + $this->options = $data['options']; + $this->modified = $data['modified']; + $this->identifiers = $data['identifiers']; + } +} diff --git a/system/src/Grav/Framework/Relationships/ToOneRelationship.php b/system/src/Grav/Framework/Relationships/ToOneRelationship.php new file mode 100644 index 0000000..9b09651 --- /dev/null +++ b/system/src/Grav/Framework/Relationships/ToOneRelationship.php @@ -0,0 +1,207 @@ + + */ +class ToOneRelationship implements ToOneRelationshipInterface +{ + /** @template-use RelationshipTrait */ + use RelationshipTrait; + use Serializable; + + /** @var IdentifierInterface|null */ + protected $identifier = null; + + public function __construct(IdentifierInterface $parent, string $name, array $options, IdentifierInterface $identifier = null) + { + $this->parent = $parent; + $this->name = $name; + + $this->parseOptions($options); + $this->replaceIdentifier($identifier); + + $this->modified = false; + } + + /** + * @return string + * @phpstan-pure + */ + public function getCardinality(): string + { + return 'to-one'; + } + + /** + * @return int + * @phpstan-pure + */ + public function count(): int + { + return $this->identifier ? 1 : 0; + } + + /** + * @return object|null + */ + public function fetch(): ?object + { + $identifier = $this->identifier; + if (is_callable([$identifier, 'getObject'])) { + $identifier = $identifier->getObject(); + } + + return $identifier; + } + + + /** + * @param string|null $id + * @param string|null $type + * @return bool + * @phpstan-pure + */ + public function has(string $id = null, string $type = null): bool + { + return $this->getIdentifier($id, $type) !== null; + } + + /** + * @param string|null $id + * @param string|null $type + * @return IdentifierInterface|null + * @phpstan-pure + */ + public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface + { + if ($id && $this->getType() === 'media' && !str_contains($id, '/')) { + $name = $this->name; + $id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id; + } + + $identifier = $this->identifier ?? null; + if (null === $identifier || ($type && $type !== $identifier->getType()) || ($id && $id !== $identifier->getId())) { + return null; + } + + return $identifier; + } + + /** + * @param string|null $id + * @param string|null $type + * @return T|null + */ + public function getObject(string $id = null, string $type = null): ?object + { + $identifier = $this->getIdentifier($id, $type); + if ($identifier && is_callable([$identifier, 'getObject'])) { + $identifier = $identifier->getObject(); + } + + return $identifier; + } + + /** + * @param IdentifierInterface $identifier + * @return bool + */ + public function addIdentifier(IdentifierInterface $identifier): bool + { + $this->identifier = $this->checkIdentifier($identifier); + $this->modified = true; + + return true; + } + + /** + * @param IdentifierInterface|null $identifier + * @return bool + */ + public function replaceIdentifier(IdentifierInterface $identifier = null): bool + { + if ($identifier === null) { + $this->identifier = null; + $this->modified = true; + + return true; + } + + return $this->addIdentifier($identifier); + } + + /** + * @param IdentifierInterface|null $identifier + * @return bool + */ + public function removeIdentifier(IdentifierInterface $identifier = null): bool + { + if (null === $identifier || $this->has($identifier->getId(), $identifier->getType())) { + $this->identifier = null; + $this->modified = true; + + return true; + } + + return false; + } + + /** + * @return iterable + * @phpstan-pure + */ + public function getIterator(): iterable + { + return new ArrayIterator((array)$this->identifier); + } + + /** + * @return array|null + */ + public function jsonSerialize(): ?array + { + return $this->identifier ? $this->identifier->jsonSerialize() : null; + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'parent' => $this->parent, + 'name' => $this->name, + 'type' => $this->type, + 'options' => $this->options, + 'modified' => $this->modified, + 'identifier' => $this->identifier, + ]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->parent = $data['parent']; + $this->name = $data['name']; + $this->type = $data['type']; + $this->options = $data['options']; + $this->modified = $data['modified']; + $this->identifier = $data['identifier']; + } +} diff --git a/system/src/Grav/Framework/Relationships/Traits/RelationshipTrait.php b/system/src/Grav/Framework/Relationships/Traits/RelationshipTrait.php new file mode 100644 index 0000000..dbe146f --- /dev/null +++ b/system/src/Grav/Framework/Relationships/Traits/RelationshipTrait.php @@ -0,0 +1,128 @@ +name; + } + + /** + * @return string + * @phpstan-pure + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return bool + * @phpstan-pure + */ + public function isModified(): bool + { + return $this->modified; + } + + /** + * @return IdentifierInterface + * @phpstan-pure + */ + public function getParent(): IdentifierInterface + { + return $this->parent; + } + + /** + * @param IdentifierInterface $identifier + * @return bool + * @phpstan-pure + */ + public function hasIdentifier(IdentifierInterface $identifier): bool + { + return $this->getIdentifier($identifier->getId(), $identifier->getType()) !== null; + } + + /** + * @return int + * @phpstan-pure + */ + abstract public function count(): int; + + /** + * @return void + * @phpstan-pure + */ + public function check(): void + { + $min = $this->options['min'] ?? 0; + $max = $this->options['max'] ?? 0; + + if ($min || $max) { + $count = $this->count(); + if ($min && $count < $min) { + throw new RuntimeException(sprintf('%s relationship has too few objects in it', $this->name)); + } + if ($max && $count > $max) { + throw new RuntimeException(sprintf('%s relationship has too many objects in it', $this->name)); + } + } + } + + /** + * @param IdentifierInterface $identifier + * @return IdentifierInterface + */ + private function checkIdentifier(IdentifierInterface $identifier): IdentifierInterface + { + if ($this->type !== $identifier->getType()) { + throw new RuntimeException(sprintf('Bad identifier type %s', $identifier->getType())); + } + + if (get_class($identifier) !== Identifier::class) { + return $identifier; + } + + if ($this->type === 'media') { + return new MediaIdentifier($identifier->getId()); + } + + return new FlexIdentifier($identifier->getId(), $identifier->getType()); + } + + private function parseOptions(array $options): void + { + $this->type = $options['type']; + $this->options = $options; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php index 36ef3a5..6c961d2 100644 --- a/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php +++ b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php @@ -1,4 +1,4 @@ -getHeaderLine('content-type'); + $method = $request->getMethod(); + if (!str_starts_with($contentType, 'multipart/form-data') || !in_array($method, ['PUT', 'PATH'], true)) { + return $handler->handle($request); + } + + $boundary = explode('; boundary=', $contentType, 2)[1] ?? ''; + $parts = explode("--{$boundary}", $request->getBody()->getContents()); + $parts = array_slice($parts, 1, count($parts) - 2); + + $params = []; + $files = []; + foreach ($parts as $part) { + $this->processPart($params, $files, $part); + } + + return $handler->handle($request->withParsedBody($params)->withUploadedFiles($files)); + } + + /** + * @param array $params + * @param array $files + * @param string $part + * @return void + */ + protected function processPart(array &$params, array &$files, string $part): void + { + $part = ltrim($part, "\r\n"); + [$rawHeaders, $body] = explode("\r\n\r\n", $part, 2); + + // Parse headers. + $rawHeaders = explode("\r\n", $rawHeaders); + $headers = array_reduce( + $rawHeaders, + static function (array $headers, $header) { + [$name, $value] = explode(':', $header); + $headers[strtolower($name)] = ltrim($value, ' '); + + return $headers; + }, + [] + ); + + if (!isset($headers['content-disposition'])) { + return; + } + + // Parse content disposition header. + $contentDisposition = $headers['content-disposition']; + preg_match('/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $contentDisposition, $matches); + $name = $matches[2]; + $filename = $matches[4] ?? null; + + if ($filename !== null) { + $stream = Stream::create($body); + $this->addFile($files, $name, new UploadedFile($stream, strlen($body), UPLOAD_ERR_OK, $filename, $headers['content-type'] ?? null)); + } elseif (strpos($contentDisposition, 'filename') !== false) { + // Not uploaded file. + $stream = Stream::create(''); + $this->addFile($files, $name, new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE)); + } else { + // Regular field. + $params[$name] = substr($body, 0, -2); + } + } + + /** + * @param array $files + * @param string $name + * @param UploadedFileInterface $file + * @return void + */ + protected function addFile(array &$files, string $name, UploadedFileInterface $file): void + { + if (strpos($name, '[]') === strlen($name) - 2) { + $name = substr($name, 0, -2); + + if (isset($files[$name]) && is_array($files[$name])) { + $files[$name][] = $file; + } else { + $files[$name] = [$file]; + } + } else { + $files[$name] = $file; + } + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php index ff82889..6b4e5c7 100644 --- a/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php +++ b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php @@ -28,10 +28,10 @@ trait RequestHandlerTrait protected $middleware; /** @var callable */ - private $handler; + protected $handler; /** @var ContainerInterface|null */ - private $container; + protected $container; /** * {@inheritdoc}