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}