diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef8e4fe..c274304 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,57 @@
+# v1.7.9
+## 03/19/2021
+
+1. [](#new)
+ * Added `Media::hide()` method to hide files from media
+ * Added `Utils::getPathFromToken()` method which works also with `Flex Objects`
+ * Added `FlexMediaTrait::getMediaField()`, which can be used to access custom media set in the blueprint fields
+ * Added `FlexMediaTrait::getFieldSettings()`, which can be used to get media field settings
+1. [](#improved)
+ * Method `Utils::getPagePathFromToken()` now calls the more generic `Utils::getPathFromToken()`
+ * Updated `SECURITY.md` to use security@getgrav.org
+1. [](#bugfix)
+ * Fixed broken media upload in `Flex` with `@self/path`, `@page` and `@theme` destinations [#3275](https://github.com/getgrav/grav/issues/3275)
+ * Fixed media fields excluding newly deleted files before saving the object
+ * Fixed method `$pages->find()` should never redirect [#3266](https://github.com/getgrav/grav/pull/3266)
+ * Fixed `Page::activeChild()` throwing an error [#3276](https://github.com/getgrav/grav/issues/3276)
+ * Fixed `Flex Page` CRUD ACL when creating a new page (needs Flex Objects plugin update) [grav-plugin-flex-objects#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115)
+ * Fixed the list of pages not showing up in admin [#3280](https://github.com/getgrav/grav/issues/3280)
+ * Fixed text field min/max validation for UTF8 characters [#3281](https://github.com/getgrav/grav/issues/3281)
+ * Fixed redirects using wrong redirect code
+
+# v1.7.8
+## 03/17/2021
+
+1. [](#new)
+ * Added `ControllerResponseTrait::createDownloadResponse()` method
+ * Added full blueprint support to theme if you move existing files in `blueprints/` to `blueprints/pages/` folder [#3255](https://github.com/getgrav/grav/issues/3255)
+ * Added support for `Theme::getFormFieldTypes()` just like in plugins
+1. [](#improved)
+ * Optimized `Flex Pages` for speed
+ * Optimized saving visible/ordered pages when there are a lot of siblings [#3231](https://github.com/getgrav/grav/issues/3231)
+ * Clearing cache now deletes all clockwork files
+ * Improved `system.pages.redirect_default_route` and `system.pages.redirect_trailing_slash` configuration options to accept redirect code
+1. [](#bugfix)
+ * Fixed clockwork error when clearing cache
+ * Fixed missing method `translated()` in `Flex Pages`
+ * Fixed missing `Flex Pages` in site if multi-language support has been enabled
+ * Fixed Grav using blueprints and form fields from disabled plugins
+ * Fixed `FlexIndex::sortBy(['key' => 'ASC'])` having no effect
+ * Fixed default Flex Pages collection ordering to order by filesystem path
+ * Fixed disappearing pages on save if `pages://` stream resolves to multiple folders where the preferred folder doesn't exist
+ * Fixed Markdown image attribute `loading` [#3251](https://github.com/getgrav/grav/pull/3251)
+ * Fixed `Uri::isValidExtension()` returning false positives
+ * Fixed `page.html` returning duplicated content with `system.pages.redirect_default_route` turned on [#3130](https://github.com/getgrav/grav/issues/3130)
+ * Fixed site redirect with redirect code failing when redirecting to sub-pages [#3035](https://github.com/getgrav/grav/pull/3035/files)
+ * Fixed `Uncaught ValueError: Path cannot be empty` when failing to upload a file [#3265](https://github.com/getgrav/grav/issues/3265)
+ * Fixed `Path cannot be empty` when viewing non-existent log file [#3270](https://github.com/getgrav/grav/issues/3270)
+ * Fixed `onAdminSave` original page having empty header [#3259](https://github.com/getgrav/grav/issues/3259)
+
# v1.7.7
## 02/23/2021
1. [](#new)
- * Added `Utils::arrayToQueryParams()` to convert an array into query params
+ * Added `Utils::arrayToQueryParams()` to convert an array into query params
1. [](#improved)
* Added original image support for all flex objects and media fields
* Improved `Pagination` class to allow custom pagination query parameter
diff --git a/SECURITY.md b/SECURITY.md
index 3a27561..30830c7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -7,9 +7,15 @@ We are focusing our security updates on the following versions
| Version | Supported |
| ------- | ------------------ |
| 1.7.x | :white_check_mark: |
-| 1.6.x | :white_check_mark: |
+| 1.6.x | :warning: |
| < 1.6 | :x: |
+## :warning: Versions
+
+Versions with :warning: will be supported for security issues, however you won't be able to update to them, you will need to manually update through the [`direct-install` command](https://learn.getgrav.org/17/admin-panel/tools).
+
+If you cannot update to the latest stable version available because, for example, your server does not meet the minimum PHP requirements, you can manually install a previous version by downloading the package from our Releases directory (https://github.com/getgrav/grav/releases).
+
## Reporting a Vulnerability
-Please contact contact@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible.
+Please contact security@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible.
diff --git a/composer.json b/composer.json
index 2d0a9cb..34d1e7e 100644
--- a/composer.json
+++ b/composer.json
@@ -26,7 +26,8 @@
"psr/simple-cache": "^1.0",
"psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0",
- "kodus/psr7-server": "*",
+ "psr/container": "~1.0.0",
+ "nyholm/psr7-server": "^1.0",
"nyholm/psr7": "^1.3",
"twig/twig": "~1.44",
"erusev/parsedown": "^1.7",
@@ -46,7 +47,7 @@
"gregwar/image": "dev-php8",
"gregwar/cache": "dev-php8",
"donatj/phpuseragentparser": "~1.1",
- "pimple/pimple": "~3.3",
+ "pimple/pimple": "~3.3.0",
"rockettheme/toolbox": "~1.5",
"maximebf/debugbar": "~1.16",
"league/climate": "^3.6",
@@ -68,7 +69,8 @@
"phpunit/php-code-coverage": "~9.2",
"victorjonsson/markdowndocs": "dev-master",
"codeception/module-asserts": "^1.3",
- "codeception/module-phpbrowser": "^1.0"
+ "codeception/module-phpbrowser": "^1.0",
+ "symfony/service-contracts": "*"
},
"replace": {
"symfony/polyfill-php72": "*",
diff --git a/composer.lock b/composer.lock
index 7f381b6..13fa182 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "32b6cbbe234714397aea3c6ed1eddf6b",
+ "content-hash": "4ae6fc7274c018b1bb34bb1b80bd62c5",
"packages": [
{
"name": "antoligy/dom-string-iterators",
@@ -381,16 +381,16 @@
},
{
"name": "donatj/phpuseragentparser",
- "version": "v1.2.0",
+ "version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/donatj/PhpUserAgent.git",
- "reference": "978e66786bc392a09b24b152a8a695dadd230e60"
+ "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/978e66786bc392a09b24b152a8a695dadd230e60",
- "reference": "978e66786bc392a09b24b152a8a695dadd230e60",
+ "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/246c1cf0a44f07168c702203bf30d5f48f17bab0",
+ "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0",
"shasum": ""
},
"require": {
@@ -433,11 +433,11 @@
],
"support": {
"issues": "https://github.com/donatj/PhpUserAgent/issues",
- "source": "https://github.com/donatj/PhpUserAgent/tree/v1.2.0"
+ "source": "https://github.com/donatj/PhpUserAgent/tree/v1.4.0"
},
"funding": [
{
- "url": "https://www.paypal.me/donatj/15",
+ "url": "https://www.paypal.me/donatj/5",
"type": "custom"
},
{
@@ -445,7 +445,7 @@
"type": "github"
}
],
- "time": "2020-12-29T05:36:08+00:00"
+ "time": "2021-03-16T16:25:14+00:00"
},
{
"name": "dragonmantank/cron-expression",
@@ -642,16 +642,16 @@
},
{
"name": "filp/whoops",
- "version": "2.9.2",
+ "version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "df7933820090489623ce0be5e85c7e693638e536"
+ "reference": "6ecda5217bf048088b891f7403b262906be5a957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536",
- "reference": "df7933820090489623ce0be5e85c7e693638e536",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/6ecda5217bf048088b891f7403b262906be5a957",
+ "reference": "6ecda5217bf048088b891f7403b262906be5a957",
"shasum": ""
},
"require": {
@@ -701,7 +701,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
- "source": "https://github.com/filp/whoops/tree/2.9.2"
+ "source": "https://github.com/filp/whoops/tree/2.10.0"
},
"funding": [
{
@@ -709,7 +709,7 @@
"type": "github"
}
],
- "time": "2021-01-24T12:00:00+00:00"
+ "time": "2021-03-16T12:00:00+00:00"
},
{
"name": "gregwar/cache",
@@ -763,18 +763,18 @@
"source": {
"type": "git",
"url": "https://github.com/getgrav/Image.git",
- "reference": "70afaa75ea19856813124142c51f5fb2e9f1a285"
+ "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getgrav/Image/zipball/70afaa75ea19856813124142c51f5fb2e9f1a285",
- "reference": "70afaa75ea19856813124142c51f5fb2e9f1a285",
+ "url": "https://api.github.com/repos/getgrav/Image/zipball/ea23859700f32447a85e79d96f331e3d6c8897a8",
+ "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8",
"shasum": ""
},
"require": {
"ext-gd": "*",
"gregwar/cache": "dev-php8",
- "php": "^5.3 || ^7.0 || ^8.0"
+ "php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"sllh/php-cs-fixer-styleci-bridge": "~1.0",
@@ -808,7 +808,7 @@
"support": {
"source": "https://github.com/getgrav/Image/tree/php8"
},
- "time": "2020-12-02T14:04:28+00:00"
+ "time": "2021-03-15T17:03:52+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -887,16 +887,16 @@
},
{
"name": "itsgoingd/clockwork",
- "version": "v5.0.6",
+ "version": "v5.0.7",
"source": {
"type": "git",
"url": "https://github.com/itsgoingd/clockwork.git",
- "reference": "1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1"
+ "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1",
- "reference": "1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1",
+ "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4",
+ "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4",
"shasum": ""
},
"require": {
@@ -944,7 +944,7 @@
],
"support": {
"issues": "https://github.com/itsgoingd/clockwork/issues",
- "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.6"
+ "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.7"
},
"funding": [
{
@@ -952,65 +952,7 @@
"type": "github"
}
],
- "time": "2020-12-27T00:18:25+00:00"
- },
- {
- "name": "kodus/psr7-server",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/kodus/psr7-server.git",
- "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/kodus/psr7-server/zipball/dcfd0116451b0f0e7c6b23b831757ed288347278",
- "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278",
- "shasum": ""
- },
- "require": {
- "php": "^7.1",
- "psr/http-factory": "^1.0",
- "psr/http-message": "^1.0"
- },
- "replace": {
- "nyholm/psr7-server": "^0.3"
- },
- "require-dev": {
- "nyholm/nsa": "^1.1",
- "nyholm/psr7": "^1.0",
- "phpunit/phpunit": "^7.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Nyholm\\Psr7Server\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com"
- },
- {
- "name": "Martijn van der Ven",
- "email": "martijn@vanderven.se"
- }
- ],
- "description": "Helper classes to handle PSR-7 server requests",
- "homepage": "http://tnyholm.se",
- "keywords": [
- "psr-17",
- "psr-7"
- ],
- "support": {
- "source": "https://github.com/kodus/psr7-server/tree/master"
- },
- "time": "2019-06-17T10:48:13+00:00"
+ "time": "2021-03-14T16:29:40+00:00"
},
{
"name": "league/climate",
@@ -1421,16 +1363,16 @@
},
{
"name": "nyholm/psr7",
- "version": "1.3.2",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/Nyholm/psr7.git",
- "reference": "a272953743c454ac4af9626634daaf5ab3ce1173"
+ "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a272953743c454ac4af9626634daaf5ab3ce1173",
- "reference": "a272953743c454ac4af9626634daaf5ab3ce1173",
+ "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b",
+ "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b",
"shasum": ""
},
"require": {
@@ -1452,7 +1394,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "1.4-dev"
}
},
"autoload": {
@@ -1482,7 +1424,73 @@
],
"support": {
"issues": "https://github.com/Nyholm/psr7/issues",
- "source": "https://github.com/Nyholm/psr7/tree/1.3.2"
+ "source": "https://github.com/Nyholm/psr7/tree/1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
+ ],
+ "time": "2021-02-18T15:41:32+00:00"
+ },
+ {
+ "name": "nyholm/psr7-server",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7-server.git",
+ "reference": "5c134aeb5dd6521c7978798663470dabf0528c96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/5c134aeb5dd6521c7978798663470dabf0528c96",
+ "reference": "5c134aeb5dd6521c7978798663470dabf0528c96",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "nyholm/nsa": "^1.1",
+ "nyholm/psr7": "^1.3",
+ "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "Helper classes to handle PSR-7 server requests",
+ "homepage": "http://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7-server/issues",
+ "source": "https://github.com/Nyholm/psr7-server/tree/1.0.1"
},
"funding": [
{
@@ -1494,7 +1502,7 @@
"type": "github"
}
],
- "time": "2020-11-14T17:35:34+00:00"
+ "time": "2020-11-15T15:26:20+00:00"
},
{
"name": "phive/twig-extensions-deferred",
@@ -2238,16 +2246,16 @@
},
{
"name": "symfony/console",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "24026c44fc37099fa145707fecd43672831b837a"
+ "reference": "c98349bda966c70d6c08b4cd8658377c94166492"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/24026c44fc37099fa145707fecd43672831b837a",
- "reference": "24026c44fc37099fa145707fecd43672831b837a",
+ "url": "https://api.github.com/repos/symfony/console/zipball/c98349bda966c70d6c08b4cd8658377c94166492",
+ "reference": "c98349bda966c70d6c08b4cd8658377c94166492",
"shasum": ""
},
"require": {
@@ -2307,7 +2315,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.19"
+ "source": "https://github.com/symfony/console/tree/v4.4.20"
},
"funding": [
{
@@ -2323,7 +2331,7 @@
"type": "tidelift"
}
],
- "time": "2021-01-27T09:09:26+00:00"
+ "time": "2021-02-22T18:44:15+00:00"
},
{
"name": "symfony/contracts",
@@ -2421,7 +2429,7 @@
},
{
"name": "symfony/event-dispatcher",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
@@ -2484,7 +2492,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.19"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.20"
},
"funding": [
{
@@ -2504,16 +2512,16 @@
},
{
"name": "symfony/http-client",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "d8df50fe9229576b254c6822eb5cfff36c02c967"
+ "reference": "67c5af7489b3c2eea771abd973243f5c58f5fb40"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/d8df50fe9229576b254c6822eb5cfff36c02c967",
- "reference": "d8df50fe9229576b254c6822eb5cfff36c02c967",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/67c5af7489b3c2eea771abd973243f5c58f5fb40",
+ "reference": "67c5af7489b3c2eea771abd973243f5c58f5fb40",
"shasum": ""
},
"require": {
@@ -2527,7 +2535,7 @@
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
- "symfony/http-client-implementation": "1.1"
+ "symfony/http-client-implementation": "1.1|2.0"
},
"require-dev": {
"guzzlehttp/promises": "^1.4",
@@ -2564,7 +2572,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.19"
+ "source": "https://github.com/symfony/http-client/tree/v4.4.20"
},
"funding": [
{
@@ -2580,7 +2588,7 @@
"type": "tidelift"
}
],
- "time": "2021-01-27T09:09:26+00:00"
+ "time": "2021-02-25T18:06:45+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -2986,7 +2994,7 @@
},
{
"name": "symfony/process",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
@@ -3027,7 +3035,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v4.4.19"
+ "source": "https://github.com/symfony/process/tree/v4.4.20"
},
"funding": [
{
@@ -3047,7 +3055,7 @@
},
{
"name": "symfony/var-dumper",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
@@ -3116,7 +3124,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v4.4.19"
+ "source": "https://github.com/symfony/var-dumper/tree/v4.4.20"
},
"funding": [
{
@@ -3136,16 +3144,16 @@
},
{
"name": "symfony/yaml",
- "version": "v4.4.19",
+ "version": "v4.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9"
+ "reference": "29e61305e1c79d25f71060903982ead8f533e267"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9",
- "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/29e61305e1c79d25f71060903982ead8f533e267",
+ "reference": "29e61305e1c79d25f71060903982ead8f533e267",
"shasum": ""
},
"require": {
@@ -3187,7 +3195,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v4.4.19"
+ "source": "https://github.com/symfony/yaml/tree/v4.4.20"
},
"funding": [
{
@@ -3203,7 +3211,7 @@
"type": "tidelift"
}
],
- "time": "2021-01-27T09:09:26+00:00"
+ "time": "2021-02-22T15:36:50+00:00"
},
{
"name": "twig/twig",
@@ -3407,16 +3415,16 @@
},
{
"name": "codeception/codeception",
- "version": "4.1.17",
+ "version": "4.1.18",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Codeception.git",
- "reference": "c153b1ab289b3e3109e685379aa8847c54ac2b68"
+ "reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeception/Codeception/zipball/c153b1ab289b3e3109e685379aa8847c54ac2b68",
- "reference": "c153b1ab289b3e3109e685379aa8847c54ac2b68",
+ "url": "https://api.github.com/repos/Codeception/Codeception/zipball/f47547bac347dfb5ea5351ff91148cbcc08e6818",
+ "reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818",
"shasum": ""
},
"require": {
@@ -3490,7 +3498,7 @@
],
"support": {
"issues": "https://github.com/Codeception/Codeception/issues",
- "source": "https://github.com/Codeception/Codeception/tree/4.1.17"
+ "source": "https://github.com/Codeception/Codeception/tree/4.1.18"
},
"funding": [
{
@@ -3498,7 +3506,7 @@
"type": "open_collective"
}
],
- "time": "2021-02-01T07:30:47+00:00"
+ "time": "2021-02-23T17:11:42+00:00"
},
{
"name": "codeception/lib-asserts",
@@ -3556,16 +3564,16 @@
},
{
"name": "codeception/lib-innerbrowser",
- "version": "1.4.0",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/Codeception/lib-innerbrowser.git",
- "reference": "b7406c710684c255d9b067d7795269a5585a0406"
+ "reference": "693e116f81ef98eae98c43ef785a726faf87394e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/b7406c710684c255d9b067d7795269a5585a0406",
- "reference": "b7406c710684c255d9b067d7795269a5585a0406",
+ "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/693e116f81ef98eae98c43ef785a726faf87394e",
+ "reference": "693e116f81ef98eae98c43ef785a726faf87394e",
"shasum": ""
},
"require": {
@@ -3610,9 +3618,9 @@
],
"support": {
"issues": "https://github.com/Codeception/lib-innerbrowser/issues",
- "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.0"
+ "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.1"
},
- "time": "2021-01-29T18:17:25+00:00"
+ "time": "2021-03-02T08:01:54+00:00"
},
{
"name": "codeception/module-asserts",
@@ -3987,16 +3995,16 @@
},
{
"name": "guzzlehttp/promises",
- "version": "1.4.0",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "60d379c243457e073cff02bc323a2a86cb355631"
+ "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
- "reference": "60d379c243457e073cff02bc323a2a86cb355631",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
+ "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
"shasum": ""
},
"require": {
@@ -4036,9 +4044,9 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/1.4.0"
+ "source": "https://github.com/guzzle/promises/tree/1.4.1"
},
- "time": "2020-09-30T07:37:28+00:00"
+ "time": "2021-03-07T09:25:29+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -4216,16 +4224,16 @@
},
{
"name": "phar-io/version",
- "version": "3.0.4",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
- "reference": "e4782611070e50613683d2b9a57730e9a3ba5451"
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451",
- "reference": "e4782611070e50613683d2b9a57730e9a3ba5451",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182",
"shasum": ""
},
"require": {
@@ -4261,9 +4269,9 @@
"description": "Library for handling version information and constraints",
"support": {
"issues": "https://github.com/phar-io/version/issues",
- "source": "https://github.com/phar-io/version/tree/3.0.4"
+ "source": "https://github.com/phar-io/version/tree/3.1.0"
},
- "time": "2020-12-13T23:18:30+00:00"
+ "time": "2021-02-23T14:00:09+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -4492,16 +4500,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "0.12.77",
+ "version": "0.12.81",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "1f10b8c8d118d01e7b492f9707999d456be5812c"
+ "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1f10b8c8d118d01e7b492f9707999d456be5812c",
- "reference": "1f10b8c8d118d01e7b492f9707999d456be5812c",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0dd5b0ebeff568f7000022ea5f04aa86ad3124b8",
+ "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8",
"shasum": ""
},
"require": {
@@ -4532,7 +4540,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
- "source": "https://github.com/phpstan/phpstan/tree/0.12.77"
+ "source": "https://github.com/phpstan/phpstan/tree/0.12.81"
},
"funding": [
{
@@ -4548,7 +4556,7 @@
"type": "tidelift"
}
],
- "time": "2021-02-17T16:22:19+00:00"
+ "time": "2021-03-08T22:03:02+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -4921,16 +4929,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.5.2",
+ "version": "9.5.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4"
+ "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f661659747f2f87f9e72095bb207bceb0f151cb4",
- "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/27241ac75fc37ecf862b6e002bf713b6566cbe41",
+ "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41",
"shasum": ""
},
"require": {
@@ -5008,7 +5016,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.2"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.3"
},
"funding": [
{
@@ -5020,7 +5028,7 @@
"type": "github"
}
],
- "time": "2021-02-02T14:45:58+00:00"
+ "time": "2021-03-17T07:30:34+00:00"
},
{
"name": "psr/http-client",
@@ -6040,16 +6048,16 @@
},
{
"name": "symfony/browser-kit",
- "version": "v5.2.3",
+ "version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd"
+ "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/b03b2057ed53ee4eab2e8f372084d7722b7b8ffd",
- "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3ca3a57ce9860318b20a924fec5daf5c6db44d93",
+ "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93",
"shasum": ""
},
"require": {
@@ -6091,7 +6099,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.2.3"
+ "source": "https://github.com/symfony/browser-kit/tree/v5.2.4"
},
"funding": [
{
@@ -6107,11 +6115,11 @@
"type": "tidelift"
}
],
- "time": "2021-01-27T12:56:27+00:00"
+ "time": "2021-02-22T06:48:33+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v5.2.3",
+ "version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@@ -6156,7 +6164,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v5.2.3"
+ "source": "https://github.com/symfony/css-selector/tree/v5.2.4"
},
"funding": [
{
@@ -6176,16 +6184,16 @@
},
{
"name": "symfony/dom-crawler",
- "version": "v5.2.3",
+ "version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384"
+ "reference": "400e265163f65aceee7e904ef532e15228de674b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/5d89ceb53ec65e1973a555072fac8ed5ecad3384",
- "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b",
+ "reference": "400e265163f65aceee7e904ef532e15228de674b",
"shasum": ""
},
"require": {
@@ -6230,7 +6238,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dom-crawler/tree/v5.2.3"
+ "source": "https://github.com/symfony/dom-crawler/tree/v5.2.4"
},
"funding": [
{
@@ -6246,20 +6254,20 @@
"type": "tidelift"
}
],
- "time": "2021-01-27T10:01:46+00:00"
+ "time": "2021-02-15T18:55:04+00:00"
},
{
"name": "symfony/finder",
- "version": "v5.2.3",
+ "version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "4adc8d172d602008c204c2e16956f99257248e03"
+ "reference": "0d639a0943822626290d169965804f79400e6a04"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03",
- "reference": "4adc8d172d602008c204c2e16956f99257248e03",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04",
+ "reference": "0d639a0943822626290d169965804f79400e6a04",
"shasum": ""
},
"require": {
@@ -6291,7 +6299,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v5.2.3"
+ "source": "https://github.com/symfony/finder/tree/v5.2.4"
},
"funding": [
{
@@ -6307,7 +6315,7 @@
"type": "tidelift"
}
],
- "time": "2021-01-28T22:06:19+00:00"
+ "time": "2021-02-15T18:55:04+00:00"
},
{
"name": "theseer/tokenizer",
@@ -6408,30 +6416,35 @@
},
{
"name": "webmozart/assert",
- "version": "1.9.1",
+ "version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
- "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
- "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0 || ^8.0",
+ "php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
- "vimeo/psalm": "<3.9.1"
+ "vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ "phpunit/phpunit": "^8.5.13"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
@@ -6455,9 +6468,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/1.9.1"
+ "source": "https://github.com/webmozarts/assert/tree/1.10.0"
},
- "time": "2020-07-08T17:02:28+00:00"
+ "time": "2021-03-09T10:59:23+00:00"
}
],
"aliases": [],
diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml
index 538fd08..d2be69c 100644
--- a/system/blueprints/config/system.yaml
+++ b/system/blueprints/config/system.yaml
@@ -177,39 +177,47 @@ form:
label: PLUGIN_ADMIN.APPEND_URL_EXT
help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP
- pages.redirect_default_route:
- type: toggle
- label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
- help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
- highlight: 0
- options:
- 1: PLUGIN_ADMIN.YES
- 0: PLUGIN_ADMIN.NO
- validate:
- type: bool
-
pages.redirect_default_code:
type: select
size: medium
classes: fancy
label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
+ default: 302
+ options:
+ 301: PLUGIN_ADMIN.REDIRECT_OPTION_301
+ 302: PLUGIN_ADMIN.REDIRECT_OPTION_302
+ 303: PLUGIN_ADMIN.REDIRECT_OPTION_303
+
+ pages.redirect_default_route:
+ type: select
+ size: medium
+ classes: fancy
+ label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
+ help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
+ default: 0
options:
- 301: 301 - Permanent
- 302: 302 - Found
- 303: 303 - Other
- 304: 304 - Not Modified
+ 0: PLUGIN_ADMIN.REDIRECT_OPTION_NO_REDIRECT
+ 1: PLUGIN_ADMIN.REDIRECT_OPTION_DEFAULT_REDIRECT
+ 301: PLUGIN_ADMIN.REDIRECT_OPTION_301
+ 302: PLUGIN_ADMIN.REDIRECT_OPTION_302
+ validate:
+ type: int
pages.redirect_trailing_slash:
- type: toggle
+ type: select
+ size: medium
+ classes: fancy
label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
- highlight: 1
+ default: 1
options:
- 1: PLUGIN_ADMIN.YES
- 0: PLUGIN_ADMIN.NO
+ 0: PLUGIN_ADMIN.REDIRECT_OPTION_NO_REDIRECT
+ 1: PLUGIN_ADMIN.REDIRECT_OPTION_DEFAULT_REDIRECT
+ 301: PLUGIN_ADMIN.REDIRECT_OPTION_301
+ 302: PLUGIN_ADMIN.REDIRECT_OPTION_302
validate:
- type: bool
+ type: int
pages.ignore_hidden:
type: toggle
@@ -1006,6 +1014,17 @@ form:
validate:
type: bool
+ assets.enable_asset_sri:
+ type: toggle
+ label: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS
+ help: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS_HELP
+ highlight: 0
+ options:
+ 1: PLUGIN_ADMIN.YES
+ 0: PLUGIN_ADMIN.NO
+ validate:
+ type: bool
+
assets.collections:
type: multilevel
label: PLUGIN_ADMIN.COLLECTIONS
diff --git a/system/blueprints/flex/pages.yaml b/system/blueprints/flex/pages.yaml
index 963ce6d..ee2e7e5 100644
--- a/system/blueprints/flex/pages.yaml
+++ b/system/blueprints/flex/pages.yaml
@@ -176,7 +176,7 @@ config:
indexed: true
# Set default ordering of the pages
ordering:
- key: ASC
+ storage_key: ASC
search:
# Search options
options:
diff --git a/system/config/system.yaml b/system/config/system.yaml
index 5c29a78..ffb98f2 100644
--- a/system/config/system.yaml
+++ b/system/config/system.yaml
@@ -75,9 +75,9 @@ pages:
last_modified: false # Set the last modified date header based on file modification timestamp
etag: true # Set the etag header tag
vary_accept_encoding: false # Add `Vary: Accept-Encoding` header
- redirect_default_route: false # Automatically redirect to a page's default route
- redirect_default_code: 302 # Default code to use for redirects
- redirect_trailing_slash: true # Handle automatically or 302 redirect a trailing / URL
+ redirect_default_code: 302 # Default code to use for redirects: 301|302|303
+ redirect_trailing_slash: 1 # Always redirect trailing slash with redirect code 0|1|301|302 (0: no redirect, 1: use default code)
+ redirect_default_route: 0 # Always redirect to page's default route using code 0|1|301|302, also removes .htm and .html extensions
ignore_files: [.DS_Store] # Files to ignore in Pages
ignore_folders: [.git, .idea] # Folders to ignore in Pages
ignore_hidden: true # Ignore all Hidden files and folders
@@ -127,6 +127,7 @@ assets: # Configuration for Assets Mana
js_pipeline_before_excludes: true # Render the pipeline before any excluded files
js_minify: true # Minify the JS during pipelining
enable_asset_timestamp: false # Enable asset timestamps
+ enable_asset_sri: false # Enable asset SRI
collections:
jquery: system://assets/jquery/jquery-2.x.min.js
diff --git a/system/defines.php b/system/defines.php
index d0fd873..e7f365a 100644
--- a/system/defines.php
+++ b/system/defines.php
@@ -8,7 +8,7 @@
// Some standard defines
define('GRAV', true);
-define('GRAV_VERSION', '1.7.7');
+define('GRAV_VERSION', '1.7.9');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);
diff --git a/system/src/Grav/Common/Assets/BaseAsset.php b/system/src/Grav/Common/Assets/BaseAsset.php
index 579b1f9..423c842 100644
--- a/system/src/Grav/Common/Assets/BaseAsset.php
+++ b/system/src/Grav/Common/Assets/BaseAsset.php
@@ -10,6 +10,7 @@
namespace Grav\Common\Assets;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
+use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
@@ -171,6 +172,31 @@ public function setPosition($position)
return $this;
}
+
+ /**
+ * Receive asset location and return the SRI integrity hash
+ *
+ * @param $input
+ *
+ * @return string
+ */
+ public static function integrityHash( $input )
+ {
+ $grav = Grav::instance();
+
+ $assetsConfig = $grav['config']->get('system.assets');
+
+ if ( !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri'] )
+ {
+ $dataToHash = file_get_contents( GRAV_ROOT . $input);
+
+ $hash = hash('sha256', $dataToHash, true);
+ $hash_base64 = base64_encode($hash);
+ return ' integrity="sha256-' . $hash_base64 . '"';
+ }
+
+ return '';
+ }
/**
diff --git a/system/src/Grav/Common/Assets/Css.php b/system/src/Grav/Common/Assets/Css.php
index b1f0a48..4c6a9c9 100644
--- a/system/src/Grav/Common/Assets/Css.php
+++ b/system/src/Grav/Common/Assets/Css.php
@@ -47,6 +47,6 @@ public function render()
return "\n";
}
- return 'renderAttributes() . ">\n";
+ return 'renderAttributes() . $this->integrityHash($this->asset) . ">\n";
}
}
diff --git a/system/src/Grav/Common/Assets/Js.php b/system/src/Grav/Common/Assets/Js.php
index 9946bd8..fc2a472 100644
--- a/system/src/Grav/Common/Assets/Js.php
+++ b/system/src/Grav/Common/Assets/Js.php
@@ -43,6 +43,6 @@ public function render()
return '\n";
}
- return '\n";
+ return '\n";
}
}
diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php
index 9b44fd8..7aef0e1 100644
--- a/system/src/Grav/Common/Assets/Pipeline.php
+++ b/system/src/Grav/Common/Assets/Pipeline.php
@@ -9,6 +9,7 @@
namespace Grav\Common\Assets;
+use Grav\Common\Assets\BaseAsset;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
@@ -148,7 +149,7 @@ public function renderCss($assets, $group, $attributes = [])
$output = "\n";
} else {
$this->asset = $relative_path;
- $output = 'renderAttributes() . ">\n";
+ $output = 'renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n";
}
return $output;
@@ -211,7 +212,7 @@ public function renderJs($assets, $group, $attributes = [])
$output = '\n";
} else {
$this->asset = $relative_path;
- $output = '\n";
+ $output = '\n";
}
return $output;
diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php
index 24f1d40..6527091 100644
--- a/system/src/Grav/Common/Cache.php
+++ b/system/src/Grav/Common/Cache.php
@@ -71,6 +71,7 @@ class Cache extends Getters
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
+ 'cache://clockwork/',
'cache://validated-',
'cache://images',
'asset://',
@@ -80,6 +81,7 @@ class Cache extends Getters
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
+ 'cache://clockwork/',
'cache://validated-',
'asset://',
];
@@ -311,7 +313,7 @@ public function getCacheDriver()
if ($password && !$redis->auth($password)) {
throw new \RedisException('Redis authentication failed');
}
-
+
// Select alternate ( !=0 ) database ID if set
if ($databaseId && !$redis->select($databaseId)) {
throw new \RedisException('Could not select alternate Redis database ID');
@@ -498,7 +500,7 @@ public static function clearCache($remove = 'standard')
$anything = true;
}
} elseif (is_dir($file)) {
- if (Folder::delete($file)) {
+ if (Folder::delete($file, false)) {
$anything = true;
}
}
diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php
index 3e0ba32..4cba620 100644
--- a/system/src/Grav/Common/Data/Validation.php
+++ b/system/src/Grav/Common/Data/Validation.php
@@ -27,7 +27,6 @@
use function is_float;
use function is_int;
use function is_string;
-use function strlen;
/**
* Class Validation
@@ -239,16 +238,20 @@ public static function typeText($value, array $params, array $field)
$value = trim($value);
}
- if (isset($params['min']) && strlen($value) < $params['min']) {
+ $len = mb_strlen($value);
+
+ $min = (int)($params['min'] ?? 0);
+ if ($min && $len < $min) {
return false;
}
- if (isset($params['max']) && strlen($value) > $params['max']) {
+ $max = (int)($params['max'] ?? 0);
+ if ($max && $len > $max) {
return false;
}
- $min = $params['min'] ?? 0;
- if (isset($params['step']) && (strlen($value) - $min) % $params['step'] === 0) {
+ $step = (int)($params['step'] ?? 0);
+ if ($step && ($len - $min) % $step === 0) {
return false;
}
@@ -271,11 +274,13 @@ protected static function filterText($value, array $params, array $field)
return '';
}
+ $value = (string)$value;
+
if (!empty($params['trim'])) {
$value = trim($value);
}
- return (string) $value;
+ return $value;
}
/**
@@ -332,7 +337,7 @@ protected static function filterLines($value, array $params, array $field)
*/
protected static function filterLower($value, array $params)
{
- return strtolower($value);
+ return mb_strtolower($value);
}
/**
@@ -342,7 +347,7 @@ protected static function filterLower($value, array $params)
*/
protected static function filterUpper($value, array $params)
{
- return strtoupper($value);
+ return mb_strtoupper($value);
}
@@ -534,7 +539,7 @@ public static function typeNumber($value, array $params, array $field)
*/
protected static function filterNumber($value, array $params, array $field)
{
- return (string)(int)$value !== (string)(float)$value ? (float) $value : (int) $value;
+ return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
}
/**
diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
index fbf0df4..d53d7f8 100644
--- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
+++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
@@ -21,13 +21,16 @@
use Grav\Common\Page\Header;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexDirectory;
-use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Grav\Framework\Flex\Pages\FlexPageIndex;
use InvalidArgumentException;
use RuntimeException;
+use function array_slice;
+use function count;
+use function in_array;
use function is_array;
use function is_string;
@@ -299,7 +302,33 @@ public function getLevelListing(array $options): array
'type' => ['root', 'dir'],
];
- return $this->getLevelListingRecurse($options);
+ $key = 'page.idx.lev.' . sha1(json_encode($options, JSON_THROW_ON_ERROR) . $this->getCacheKey());
+ $checksum = $this->getCacheChecksum();
+
+ $cache = $this->getCache('object');
+
+ /** @var Debugger $debugger */
+ $debugger = Grav::instance()['debugger'];
+
+ $result = null;
+ try {
+ $cached = $cache->get($key);
+ $test = $cached[0] ?? null;
+ $result = $test === $checksum ? ($cached[1] ?? null) : null;
+ } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
+ $debugger->addException($e);
+ }
+
+ try {
+ if (null === $result) {
+ $result = $this->getLevelListingRecurse($options);
+ $cache->set($key, [$checksum, $result]);
+ }
+ } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
+ $debugger->addException($e);
+ }
+
+ return $result;
}
/**
@@ -429,6 +458,9 @@ protected function getLevelListingRecurse(array $options): array
$selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null);
}
+ /** @var UserInterface|null $user */
+ $user = Grav::instance()['user'] ?? null;
+
/** @var PageObject $child */
foreach ($selectedChildren as $child) {
$selected = $child->path() === $extra;
@@ -482,7 +514,7 @@ protected function getLevelListingRecurse(array $options): array
'visible' => $child->visible(),
'routable' => $child->routable(),
'tags' => $tags,
- 'actions' => $this->getListingActions($child),
+ 'actions' => $this->getListingActions($child, $user),
];
$extras = array_filter($extras, static function ($v) {
return $v !== null;
@@ -490,12 +522,13 @@ protected function getLevelListingRecurse(array $options): array
$tmp = $child->children()->getIndex();
$child_count = $tmp->count();
$count = $filters ? $tmp->filterBy($filters, true)->count() : null;
+ $route = $child->getRoute();
$payload = [
'item-key' => basename($child->rawRoute() ?? $child->getKey()),
'icon' => $icon,
'title' => htmlspecialchars($child->menu()),
'route' => [
- 'display' => $child->getRoute()->toString(false) ?: '/',
+ 'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
'raw' => $child->rawRoute(),
],
'modified' => $this->jsDate($child->modified()),
@@ -535,20 +568,21 @@ protected function getLevelListingRecurse(array $options): array
/**
* @param PageObject $object
+ * @param UserInterface $user
* @return array
*/
- protected function getListingActions(PageObject $object): array
+ protected function getListingActions(PageObject $object, UserInterface $user): array
{
$actions = [];
- if ($object->isAuthorized('read')) {
+ if ($object->isAuthorized('read', null, $user)) {
$actions[] = 'preview';
$actions[] = 'edit';
}
- if ($object->isAuthorized('update')) {
+ if ($object->isAuthorized('update', null, $user)) {
$actions[] = 'copy';
$actions[] = 'move';
}
- if ($object->isAuthorized('delete')) {
+ if ($object->isAuthorized('delete', null, $user)) {
$actions[] = 'delete';
}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
index 56c0527..9ed8b67 100644
--- a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
+++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
@@ -22,6 +22,7 @@
use Grav\Common\Language\Language;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
+use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\FlexObject;
@@ -62,7 +63,6 @@ class PageObject extends FlexPageObject
/** @var string Language code, eg: 'en' */
protected $language;
-
/** @var string File format, eg. 'md' */
protected $format;
@@ -78,6 +78,7 @@ public static function getCachedMethods(): array
'path' => true,
'full_order' => true,
'filterBy' => true,
+ 'translated' => false,
] + parent::getCachedMethods();
}
@@ -92,6 +93,11 @@ public function initialize(): void
}
}
+ public function translated(): bool
+ {
+ return $this->translatedLanguages(true) ? true : false;
+ }
+
/**
* @param string|array $query
* @return Route|null
@@ -223,7 +229,7 @@ protected function onBeforeSave(array $variables)
}
// Reorder siblings.
- $siblings = is_array($reorder) ? $this->reorderSiblings($reorder) : [];
+ $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
$data = $this->prepareStorage();
unset($data['header']);
@@ -289,6 +295,9 @@ public function save($reorder = true)
$grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
}
+ // Reset original after save events have all been called.
+ $this->_original = null;
+
return $instance;
}
@@ -308,13 +317,36 @@ public function move(PageInterface $parent)
$this->_reorder = [];
$this->setProperty('parent_key', $parent->getStorageKey());
+ $this->storeOriginal();
return $this;
}
+ /**
+ * @param UserInterface $user
+ * @param string $action
+ * @param string $scope
+ * @param bool $isMe
+ * @return bool|null
+ */
+ protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
+ {
+ // Special case: creating a new page means checking parent for its permissions.
+ if ($action === 'create' && !$this->exists()) {
+ $parent = $this->parent();
+ if ($parent && method_exists($parent, 'isAuthorized')) {
+ return $parent->isAuthorized($action, $scope, $user);
+ }
+
+ return false;
+ }
+
+ return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
+ }
+
/**
* @param array $ordering
- * @return PageCollection
+ * @return PageCollection|null
*/
protected function reorderSiblings(array $ordering)
{
@@ -324,17 +356,35 @@ protected function reorderSiblings(array $ordering)
$newParentKey = $this->getProperty('parent_key');
$isMoved = $oldParentKey !== $newParentKey;
$order = !$isMoved ? $this->order() : false;
+ if ($order !== false) {
+ $order = (int)$order;
+ }
$parent = $this->parent();
if (!$parent) {
throw new RuntimeException('Cannot reorder a page which has no parent');
}
- /** @var PageCollection|null $siblings */
+ /** @var PageCollection $siblings */
$siblings = $parent->children();
+ $siblings = $siblings->getCollection()->withOrdered();
+
+ // Handle special case where ordering isn't given.
+ if ($ordering === []) {
+ if ($order >= 999999) {
+ // Set ordering to point to be the last item.
+ $order = 0;
+ foreach ($siblings as $sibling) {
+ $order = max($order, (int)$sibling->order());
+ }
+ $this->order($order + 1);
+ }
- /** @var PageCollection|null $siblings */
- $siblings = $siblings->getCollection()->withOrdered()->orderBy(['order' => 'ASC']);
+ // Do not change sibling ordering.
+ return null;
+ }
+
+ $siblings = $siblings->orderBy(['order' => 'ASC']);
if ($storageKey !== null) {
if ($order !== false) {
@@ -378,7 +428,9 @@ protected function reorderSiblings(array $ordering)
throw new RuntimeException("New parent page '{$parentKey}' not found.");
}
}
- $newSiblings = $newParent->children()->getCollection()->withOrdered();
+ /** @var PageCollection $newSiblings */
+ $newSiblings = $newParent->children();
+ $newSiblings = $newSiblings->getCollection()->withOrdered();
$order = 0;
foreach ($newSiblings as $sibling) {
$order = max($order, (int)$sibling->order());
@@ -584,14 +636,17 @@ protected function filterElements(array &$elements, bool $extended = false): voi
unset($elements['ordering'], $elements['order']);
} elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
// Store ordering.
- $this->_reorder = !empty($elements['order']) ? explode(',', $elements['order']) : [];
+ $ordering = $elements['order'] ?? null;
+ $this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
$order = false;
if ((bool)($elements['ordering'] ?? false)) {
- $order = 999999;
+ $order = $this->order();
+ if ($order === false) {
+ $order = 999999;
+ }
}
- $this->order();
$elements['order'] = $order;
}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
index a235174..0ccb856 100644
--- a/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
+++ b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
@@ -110,6 +110,9 @@ public function readFrontmatter(string $key): string
}
} catch (RuntimeException $e) {
$frontmatter = 'ERROR: ' . $e->getMessage();
+ } finally {
+ $file->free();
+ unset($file);
}
return $frontmatter;
@@ -127,6 +130,9 @@ public function readRaw(string $key): string
$raw = $file->raw();
} catch (RuntimeException $e) {
$raw = 'ERROR: ' . $e->getMessage();
+ } finally {
+ $file->free();
+ unset($file);
}
return $raw;
@@ -407,7 +413,7 @@ protected function saveRow(string $key, array $row): array
if (!$isClone && $file->exists()) {
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $toPath = $locator->isStream($newFilepath) ? $locator->findResource($newFilepath, true, true) : $newFilepath;
+ $toPath = $locator->isStream($newFilepath) ? $locator->findResource($newFilepath, true, true) : GRAV_ROOT . "/{$newFilepath}";
$success = $file->rename($toPath);
if (!$success) {
throw new RuntimeException("Changing page template failed: {$oldFilepath} => {$newFilepath}");
@@ -439,16 +445,19 @@ protected function saveRow(string $key, array $row): array
} else {
$debugger->addMessage('Page content has not been changed, do not update the file', 'debug');
}
-
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($newFolder)) {
- $locator->clearCache();
- }
} catch (RuntimeException $e) {
$name = isset($file) ? $file->filename() : $newKey;
throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $name, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
+
+ if (isset($file)) {
+ $file->free();
+ unset($file);
+ }
}
$row['__META'] = $this->getObjectMeta($newKey, true);
@@ -512,7 +521,11 @@ protected function getObjectMeta(string $key, bool $reload = false): array
$locator = Grav::instance()['locator'];
if (mb_strpos($key, '@@') === false) {
$path = $this->getStoragePath($key);
- $path = $path ? $locator->findResource($path) : null;
+ if (is_string($path)) {
+ $path = $locator->isStream($path) ? $locator->findResource($path) : GRAV_ROOT . "/{$path}";
+ } else {
+ $path = null;
+ }
} else {
$path = null;
}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
index 341ac4d..3b490a5 100644
--- a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
@@ -15,6 +15,7 @@
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
+use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Filesystem\Filesystem;
use RuntimeException;
@@ -97,6 +98,7 @@ public function active(): bool
public function activeChild(): bool
{
$grav = Grav::instance();
+ /** @var Uri $uri */
$uri = $grav['uri'];
/** @var Pages $pages */
$pages = $grav['pages'];
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
index e2de494..dff42c9 100644
--- a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
@@ -67,7 +67,7 @@ public function translatedLanguages($onlyPublished = false): array
if (!$folder) {
return [];
}
- $folder = $locator($folder);
+ $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
$list = array_fill_keys($languages, null);
foreach ($translated as $languageCode => $languageFile) {
diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php
index 048f7d4..c9097ee 100644
--- a/system/src/Grav/Common/Grav.php
+++ b/system/src/Grav/Common/Grav.php
@@ -426,12 +426,18 @@ public function getRedirectResponse($route, $code = null): ResponseInterface
// Clean route for redirect
$route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
- // Check for code in route
- $regex = '/.*(\[(30[1-7])\])$/';
- preg_match($regex, $route, $matches);
- if ($matches) {
- $route = str_replace($matches[1], '', $matches[0]);
- $code = $matches[2];
+ if ($code < 300 || $code > 399) {
+ $code = null;
+ }
+
+ if (null === $code) {
+ // Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html
+ $regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/';
+ preg_match($regex, $route, $matches);
+ if ($matches) {
+ $route = str_replace($matches[1], '', $matches[0]);
+ $code = $matches[2];
+ }
}
if ($code === null) {
diff --git a/system/src/Grav/Common/Helpers/LogViewer.php b/system/src/Grav/Common/Helpers/LogViewer.php
index 1e3e8d2..a03fde8 100644
--- a/system/src/Grav/Common/Helpers/LogViewer.php
+++ b/system/src/Grav/Common/Helpers/LogViewer.php
@@ -34,7 +34,7 @@ class LogViewer
public function objectTail($filepath, $lines = 1, $desc = true)
{
$data = $this->tail($filepath, $lines);
- $tailed_log = explode(PHP_EOL, $data);
+ $tailed_log = $data ? explode(PHP_EOL, $data) : [];
$line_objects = [];
foreach ($tailed_log as $line) {
@@ -54,13 +54,13 @@ public function objectTail($filepath, $lines = 1, $desc = true)
public function tail($filepath, $lines = 1)
{
- $f = @fopen($filepath, "rb");
+ $f = $filepath ? @fopen($filepath, 'rb') : false;
if ($f === false) {
return false;
- } else {
- $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
}
+ $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
+
fseek($f, -1, SEEK_END);
if (fread($f, 1) != "\n") {
$lines -= 1;
diff --git a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
index 8fc3c20..2900266 100644
--- a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
+++ b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
@@ -63,7 +63,7 @@ public function url($include_host = false, $canonical = false, $include_lang = t
* the parents route and the current Page's slug.
*
* @param string|null $var Set new default route.
- * @return string The route for the Page.
+ * @return string|null The route for the Page.
*/
public function route($var = null);
diff --git a/system/src/Grav/Common/Page/Markdown/Excerpts.php b/system/src/Grav/Common/Page/Markdown/Excerpts.php
index 1a36602..3317217 100644
--- a/system/src/Grav/Common/Page/Markdown/Excerpts.php
+++ b/system/src/Grav/Common/Page/Markdown/Excerpts.php
@@ -278,10 +278,10 @@ static function ($carry, $item) {
);
}
- $defaults = $config['images']['defaults'] ?? [];
+ $defaults = $this->config['images']['defaults'] ?? [];
if (count($defaults)) {
foreach ($defaults as $method => $params) {
- if (!array_search($method, array_column($actions, 'method'))) {
+ if (array_search($method, array_column($actions, 'method')) === false) {
$actions[] = [
'method' => $method,
'params' => $params,
diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
index 06b44a7..db81ddd 100644
--- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php
+++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
@@ -198,6 +198,17 @@ public function add($name, $file)
}
}
+ /**
+ * @param string $name
+ * @return void
+ */
+ public function hide($name)
+ {
+ $this->offsetUnset($name);
+
+ unset($this->images[$name], $this->videos[$name], $this->audios[$name], $this->files[$name]);
+ }
+
/**
* Create Medium from a file.
*
diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php
index 9d037d2..5c92cba 100644
--- a/system/src/Grav/Common/Page/Page.php
+++ b/system/src/Grav/Common/Page/Page.php
@@ -1875,7 +1875,7 @@ public function url($include_host = false, $canonical = false, $include_base = t
* the parents route and the current Page's slug.
*
* @param string|null $var Set new default route.
- * @return string The route for the Page.
+ * @return string|null The route for the Page.
*/
public function route($var = null)
{
@@ -2496,21 +2496,23 @@ public function active()
*/
public function activeChild()
{
- $uri = Grav::instance()['uri'];
- $pages = Grav::instance()['pages'];
+ $grav = Grav::instance();
+ /** @var Uri $uri */
+ $uri = $grav['uri'];
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
$uri_path = rtrim(urldecode($uri->path()), '/');
- $routes = Grav::instance()['pages']->routes();
+ $routes = $pages->routes();
if (isset($routes[$uri_path])) {
+ $page = $pages->find($uri->route());
/** @var PageInterface|null $child_page */
- $child_page = $pages->find($uri->route())->parent();
- if ($child_page) {
- while (!$child_page->root()) {
- if ($this->path() === $child_page->path()) {
- return true;
- }
- $child_page = $child_page->parent();
+ $child_page = $page ? $page->parent() : null;
+ while ($child_page && !$child_page->root()) {
+ if ($this->path() === $child_page->path()) {
+ return true;
}
+ $child_page = $child_page->parent();
}
}
diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php
index 0c430fd..3b3ce8e 100644
--- a/system/src/Grav/Common/Page/Pages.php
+++ b/system/src/Grav/Common/Page/Pages.php
@@ -769,6 +769,9 @@ public function sortCollection(Collection $collection, $orderBy, $orderDir = 'as
public function get($path)
{
$path = (string)$path;
+ if ($path === '') {
+ return null;
+ }
// Check for local instances first.
if (array_key_exists($path, $this->instances)) {
@@ -777,14 +780,26 @@ public function get($path)
$instance = $this->index[$path] ?? null;
if (is_string($instance)) {
- /** @var Language $language */
- $language = $this->grav['language'];
- $lang = $language->getActive();
- if ($lang) {
- $instance .= ':' . $lang;
+ if ($this->directory) {
+ /** @var Language $language */
+ $language = $this->grav['language'];
+ $lang = $language->getActive();
+ if ($lang) {
+ $languages = $language->getFallbackLanguages($lang, true);
+ $key = $instance;
+ $instance = null;
+ foreach ($languages as $code) {
+ $test = $code ? $key . ':' . $code : $key;
+ if (($instance = $this->directory->getObject($test, 'flex_key')) !== null) {
+ break;
+ }
+ }
+ } else {
+ $instance = $this->directory->getObject($instance, 'flex_key');
+ }
}
- $instance = $this->directory ? $this->directory->getObject($instance, 'flex_key') : null;
- if ($instance) {
+
+ if ($instance instanceof PageInterface) {
if ($this->fire_events && method_exists($instance, 'initialize')) {
$instance->initialize();
}
@@ -865,103 +880,146 @@ public function inherited($route, $field = null)
}
/**
- * alias method to return find a page.
+ * Find a page based on route.
*
- * @param string $route The relative URL of the page
- * @param bool $all
+ * @param string $route The route of the page
+ * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable
* @return PageInterface|null
*/
public function find($route, $all = false)
{
- return $this->dispatch($route, $all, false);
+ $route = urldecode((string)$route);
+
+ // Fetch page if there's a defined route to it.
+ $path = $this->routes[$route] ?? null;
+ $page = null !== $path ? $this->get($path) : null;
+
+ // Try without trailing slash
+ if (null === $page && Utils::endsWith($route, '/')) {
+ $path = $this->routes[rtrim($route, '/')] ?? null;
+ $page = null !== $path ? $this->get($path) : null;
+ }
+
+ if (!$all && !isset($this->grav['admin'])) {
+ if (null === $page || !$page->routable()) {
+ // If the page cannot be accessed, look for the site wide routes and wildcards.
+ $page = $this->findSiteBasedRoute($route) ?? $page;
+ }
+ }
+
+ return $page;
+ }
+
+ /**
+ * Check site based routes.
+ *
+ * @param string $route
+ * @return PageInterface|null
+ */
+ protected function findSiteBasedRoute($route)
+ {
+ /** @var Config $config */
+ $config = $this->grav['config'];
+
+ $site_routes = $config->get('site.routes');
+ if (!is_array($site_routes)) {
+ return null;
+ }
+
+ $page = null;
+
+ // See if route matches one in the site configuration
+ $site_route = $site_routes[$route] ?? null;
+ if ($site_route) {
+ $page = $this->find($site_route);
+ } else {
+ // Use reverse order because of B/C (previously matched multiple and returned the last match).
+ foreach (array_reverse($site_routes, true) as $pattern => $replace) {
+ $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
+ try {
+ $found = preg_replace($pattern, $replace, $route);
+ if ($found && $found !== $route) {
+ $page = $this->find($found);
+ if ($page) {
+ return $page;
+ }
+ }
+ } catch (ErrorException $e) {
+ $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
+ }
+ }
+ }
+
+ return $page;
}
/**
* Dispatch URI to a page.
*
* @param string $route The relative URL of the page
- * @param bool $all
- * @param bool $redirect
+ * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable
+ * @param bool $redirect If true, allow redirects
* @return PageInterface|null
* @throws Exception
*/
public function dispatch($route, $all = false, $redirect = true)
{
- $route = urldecode($route);
+ $page = $this->find($route, true);
- // Fetch page if there's a defined route to it.
- $path = $this->routes[$route] ?? null;
- $page = null !== $path ? $this->get($path) : null;
- // Try without trailing slash
- if (!$page && Utils::endsWith($route, '/')) {
- $path = $this->routes[rtrim($route, '/')] ?? null;
- $page = null !== $path ? $this->get($path) : null;
+ // If we want all pages or are in admin, return what we already have.
+ if ($all || isset($this->grav['admin'])) {
+ return $page;
}
- // Are we in the admin? this is important!
- $not_admin = !isset($this->grav['admin']);
+ if ($page) {
+ $routable = $page->routable();
+ if ($redirect) {
+ if ($page->redirect()) {
+ // Follow a redirect page.
+ $this->grav->redirectLangSafe($page->redirect());
+ }
+
+ if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) {
+ // Redirect to the first visible child as current page isn't routable.
+ $this->grav->redirectLangSafe($child->route());
+ }
+ }
- // If the page cannot be reached, look into site wide redirects, routes + wildcards
- if (!$all && $not_admin) {
- // If the page is a simple redirect, just do it.
- if ($redirect && $page && $page->redirect()) {
- $this->grav->redirectLangSafe($page->redirect());
+ if ($routable) {
+ return $page;
}
+ }
- // fall back and check site based redirects
- if (!$page || !$page->routable()) {
- // Redirect to the first child (placeholder page)
- if ($redirect && $page && count($children = $page->children()->visible()->routable()->published()) > 0) {
- $this->grav->redirectLangSafe($children->first()->route());
- }
+ $route = urldecode((string)$route);
- /** @var Config $config */
- $config = $this->grav['config'];
+ // The page cannot be reached, look into site wide redirects, routes and wildcards.
+ $redirectedPage = $this->findSiteBasedRoute($route);
+ if ($redirectedPage) {
+ $page = $this->dispatch($redirectedPage->route(), false, $redirect);
+ }
- // See if route matches one in the site configuration
- $site_route = $config->get("site.routes.{$route}");
- if ($site_route) {
- $page = $this->dispatch($site_route, $all, $redirect);
- } else {
- /** @var Uri $uri */
- $uri = $this->grav['uri'];
- /** @var \Grav\Framework\Uri\Uri $source_url */
- $source_url = $uri->uri(false);
-
- // Try Regex style redirects
- $site_redirects = $config->get('site.redirects');
- if (is_array($site_redirects)) {
- foreach ((array)$site_redirects as $pattern => $replace) {
- $pattern = ltrim($pattern, '^');
- $pattern = '#^' . str_replace('/', '\/', $pattern) . '#';
- try {
- /** @var string $found */
- $found = preg_replace($pattern, $replace, $source_url);
- if ($found && $found !== $source_url) {
- $this->grav->redirectLangSafe($found);
- }
- } catch (ErrorException $e) {
- $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
- }
- }
- }
+ /** @var Config $config */
+ $config = $this->grav['config'];
- // Try Regex style routes
- $site_routes = $config->get('site.routes');
- if (is_array($site_routes)) {
- foreach ((array)$site_routes as $pattern => $replace) {
- $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
- try {
- /** @var string $found */
- $found = preg_replace($pattern, $replace, $source_url);
- if ($found && $found !== $source_url) {
- $page = $this->dispatch($found, $all, $redirect);
- }
- } catch (ErrorException $e) {
- $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
- }
- }
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+ /** @var \Grav\Framework\Uri\Uri $source_url */
+ $source_url = $uri->uri(false);
+
+ // Try Regex style redirects
+ $site_redirects = $config->get('site.redirects');
+ if (is_array($site_redirects)) {
+ foreach ((array)$site_redirects as $pattern => $replace) {
+ $pattern = ltrim($pattern, '^');
+ $pattern = '#^' . str_replace('/', '\/', $pattern) . '#';
+ try {
+ /** @var string $found */
+ $found = preg_replace($pattern, $replace, $source_url);
+ if ($found && $found !== $source_url) {
+ $this->grav->redirectLangSafe($found);
}
+ } catch (ErrorException $e) {
+ $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
}
}
}
@@ -1159,7 +1217,14 @@ public static function getTypes()
$event->types = $types;
$grav->fireEvent('onGetPageBlueprints', $event);
- $types->scanBlueprints('theme://blueprints/');
+ $types->init();
+
+ // Try new location first.
+ $lookup = 'theme://blueprints/pages/';
+ if (!is_dir($lookup)) {
+ $lookup = 'theme://blueprints/';
+ }
+ $types->scanBlueprints($lookup);
// Scan templates
$event = new Event();
diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php
index 051610f..b9d8f95 100644
--- a/system/src/Grav/Common/Page/Types.php
+++ b/system/src/Grav/Common/Page/Types.php
@@ -32,22 +32,23 @@ class Types implements \ArrayAccess, \Iterator, \Countable
/** @var array */
protected $items;
/** @var array */
- protected $systemBlueprints;
+ protected $systemBlueprints = [];
/**
* @param string $type
* @param Blueprint|null $blueprint
+ * @return void
*/
public function register($type, $blueprint = null)
{
if (!isset($this->items[$type])) {
$this->items[$type] = [];
- } elseif (!$blueprint) {
+ } elseif (null === $blueprint) {
return;
}
- if (!$blueprint && $this->systemBlueprints) {
- $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default'];
+ if (null === $blueprint) {
+ $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default'] ?? null;
}
if ($blueprint) {
@@ -55,8 +56,23 @@ public function register($type, $blueprint = null)
}
}
+ /**
+ * @return void
+ */
+ public function init()
+ {
+ if (null === $this->systemBlueprints) {
+ // Register all blueprints from the blueprints stream.
+ $this->systemBlueprints = $this->findBlueprints('blueprints://pages');
+ foreach ($this->systemBlueprints as $type => $blueprint) {
+ $this->register($type);
+ }
+ }
+ }
+
/**
* @param string $uri
+ * @return void
*/
public function scanBlueprints($uri)
{
@@ -64,15 +80,6 @@ public function scanBlueprints($uri)
throw new InvalidArgumentException('First parameter must be URI');
}
- if (null === $this->systemBlueprints) {
- $this->systemBlueprints = $this->findBlueprints('blueprints://pages');
-
- // Register default by default.
- $this->register('default');
-
- $this->register('external');
- }
-
foreach ($this->findBlueprints($uri) as $type => $blueprint) {
$this->register($type, $blueprint);
}
@@ -80,6 +87,7 @@ public function scanBlueprints($uri)
/**
* @param string $uri
+ * @return void
*/
public function scanTemplates($uri)
{
diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php
index d075860..65b11db 100644
--- a/system/src/Grav/Common/Plugin.php
+++ b/system/src/Grav/Common/Plugin.php
@@ -16,6 +16,7 @@
use Grav\Common\Config\Config;
use LogicException;
use RocketTheme\Toolbox\File\YamlFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use function defined;
@@ -35,11 +36,11 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
/** @var Grav */
protected $grav;
- /** @var Config */
+ /** @var Config|null */
protected $config;
/** @var bool */
protected $active = true;
- /** @var Blueprint */
+ /** @var Blueprint|null */
protected $blueprint;
/**
@@ -127,21 +128,25 @@ public function isCli()
*/
protected function isPluginActiveAdmin($plugin_route)
{
- $should_run = false;
+ $active = false;
+ /** @var Uri $uri */
$uri = $this->grav['uri'];
+ /** @var Config $config */
+ $config = $this->config ?? $this->grav['config'];
- if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
- $should_run = false;
+ if (strpos($uri->path(), $config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
+ $active = false;
} elseif (isset($uri->paths()[1]) && $uri->paths()[1] === $plugin_route) {
- $should_run = true;
+ $active = true;
}
- return $should_run;
+ return $active;
}
/**
* @param array $events
+ * @return void
*/
protected function enable(array $events)
{
@@ -164,22 +169,18 @@ protected function enable(array $events)
/**
* @param array $params
* @param string $eventName
+ * @return int
*/
private function getPriority($params, $eventName)
{
- $grav = Grav::instance();
$override = implode('.', ['priorities', $this->name, $eventName, $params[0]]);
- if ($grav['config']->get($override) !== null) {
- return $grav['config']->get($override);
- }
- if (isset($params[1])) {
- return $params[1];
- }
- return 0;
+
+ return $this->grav['config']->get($override) ?? $params[1] ?? 0;
}
/**
* @param array $events
+ * @return void
*/
protected function disable(array $events)
{
@@ -207,12 +208,13 @@ protected function disable(array $events)
*/
public function offsetExists($offset)
{
- $this->loadBlueprint();
-
if ($offset === 'title') {
$offset = 'name';
}
- return isset($this->blueprint[$offset]);
+
+ $blueprint = $this->getBlueprint();
+
+ return isset($blueprint[$offset]);
}
/**
@@ -223,12 +225,13 @@ public function offsetExists($offset)
*/
public function offsetGet($offset)
{
- $this->loadBlueprint();
-
if ($offset === 'title') {
$offset = 'name';
}
- return $this->blueprint[$offset] ?? null;
+
+ $blueprint = $this->getBlueprint();
+
+ return $blueprint[$offset] ?? null;
}
/**
@@ -281,9 +284,12 @@ public function __debugInfo(): array
*/
protected function parseLinks($content, $function, $internal_regex = '(.*)')
{
- $regex = '/\[plugin:(?:' . $this->name . ')\]\(' . $internal_regex . '\)/i';
+ $regex = '/\[plugin:(?:' . preg_quote($this->name, '/') . ')\]\(' . $internal_regex . '\)/i';
+
+ $result = preg_replace_callback($regex, $function, $content);
+ \assert($result !== null);
- return preg_replace_callback($regex, $function, $content);
+ return $result;
}
/**
@@ -301,9 +307,12 @@ protected function parseLinks($content, $function, $internal_regex = '(.*)')
*/
protected function mergeConfig(PageInterface $page, $deep = false, $params = [], $type = 'plugins')
{
+ /** @var Config $config */
+ $config = $this->config ?? $this->grav['config'];
+
$class_name = $this->name;
$class_name_merged = $class_name . '.merged';
- $defaults = $this->config->get($type . '.' . $class_name, []);
+ $defaults = $config->get($type . '.' . $class_name, []);
$page_header = $page->header();
$header = [];
@@ -356,23 +365,26 @@ private function mergeArrays($deep, $array1, $array2)
/**
* Persists to disk the plugin parameters currently stored in the Grav Config object
*
- * @param string $plugin_name The name of the plugin whose config it should store.
- *
+ * @param string $name The name of the plugin whose config it should store.
* @return bool
*/
- public static function saveConfig($plugin_name)
+ public static function saveConfig($name)
{
- if (!$plugin_name) {
+ if (!$name) {
return false;
}
$grav = Grav::instance();
+
+ /** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $filename = 'config://plugins/' . $plugin_name . '.yaml';
- $file = YamlFile::instance($locator->findResource($filename, true, true));
- $content = $grav['config']->get('plugins.' . $plugin_name);
+
+ $filename = 'config://plugins/' . $name . '.yaml';
+ $file = YamlFile::instance((string)$locator->findResource($filename, true, true));
+ $content = $grav['config']->get('plugins.' . $name);
$file->save($content);
$file->free();
+ unset($file);
return true;
}
@@ -384,21 +396,28 @@ public static function saveConfig($plugin_name)
*/
public function getBlueprint()
{
- if (!$this->blueprint) {
+ if (null === $this->blueprint) {
$this->loadBlueprint();
+ \assert($this->blueprint instanceof Blueprint);
}
+
return $this->blueprint;
}
/**
* Load blueprints.
+ *
+ * @return void
*/
protected function loadBlueprint()
{
- if (!$this->blueprint) {
+ if (null === $this->blueprint) {
$grav = Grav::instance();
+ /** @var Plugins $plugins */
$plugins = $grav['plugins'];
- $this->blueprint = $plugins->get($this->name)->blueprints();
+ $data = $plugins->get($this->name);
+ \assert($data !== null);
+ $this->blueprint = $data->blueprints();
}
}
}
diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php
index f1d7b8e..be8f43b 100644
--- a/system/src/Grav/Common/Plugins.php
+++ b/system/src/Grav/Common/Plugins.php
@@ -17,6 +17,7 @@
use Grav\Events\PluginsLoadedEvent;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
+use SplFileInfo;
use Symfony\Component\EventDispatcher\EventDispatcher;
use function get_class;
use function is_object;
@@ -27,7 +28,7 @@
*/
class Plugins extends Iterator
{
- /** @var array */
+ /** @var array|null */
public $formFieldTypes;
/** @var bool */
@@ -46,6 +47,7 @@ public function __construct()
$iterator = $locator->getIterator('plugins://');
$plugins = [];
+ /** @var SplFileInfo $directory */
foreach ($iterator as $directory) {
if (!$directory->isDir()) {
continue;
@@ -56,7 +58,10 @@ public function __construct()
sort($plugins, SORT_NATURAL | SORT_FLAG_CASE);
foreach ($plugins as $plugin) {
- $this->add($this->loadPlugin($plugin));
+ $object = $this->loadPlugin($plugin);
+ if ($object) {
+ $this->add($object);
+ }
}
}
@@ -68,13 +73,21 @@ public function setup()
$blueprints = [];
$formFields = [];
+ $grav = Grav::instance();
+
+ /** @var Config $config */
+ $config = $grav['config'];
+
/** @var Plugin $plugin */
foreach ($this->items as $plugin) {
- if (isset($plugin->features['blueprints'])) {
- $blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints'];
- }
- if (method_exists($plugin, 'getFormFieldTypes')) {
- $formFields[get_class($plugin)] = isset($plugin->features['formfields']) ? $plugin->features['formfields'] : 0;
+ // Setup only enabled plugins.
+ if ($config["plugins.{$plugin->name}.enabled"] && $plugin instanceof Plugin) {
+ if (isset($plugin->features['blueprints'])) {
+ $blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints'];
+ }
+ if (method_exists($plugin, 'getFormFieldTypes')) {
+ $formFields[get_class($plugin)] = $plugin->features['formfields'] ?? 0;
+ }
}
}
@@ -83,7 +96,7 @@ public function setup()
arsort($blueprints, SORT_NUMERIC);
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
+ $locator = $grav['locator'];
$locator->addPath('blueprints', '', array_keys($blueprints), ['system', 'blueprints']);
}
@@ -150,6 +163,7 @@ public function init()
* Add a plugin
*
* @param Plugin $plugin
+ * @return void
*/
public function add($plugin)
{
@@ -175,8 +189,8 @@ public function __debugInfo(): array
*/
public static function getPlugins(): array
{
- $grav = Grav::instance();
- $plugins = $grav['plugins'];
+ /** @var Plugins $plugins */
+ $plugins = Grav::instance()['plugins'];
$list = [];
foreach ($plugins as $instance) {
@@ -200,11 +214,13 @@ public static function getPlugin(string $name)
/**
* Return list of all plugin data with their blueprints.
*
- * @return array
+ * @return Data[]
*/
public static function all()
{
$grav = Grav::instance();
+
+ /** @var Plugins $plugins */
$plugins = $grav['plugins'];
$list = [];
diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php
index a6a606c..ea23fa8 100644
--- a/system/src/Grav/Common/Processors/InitializeProcessor.php
+++ b/system/src/Grav/Common/Processors/InitializeProcessor.php
@@ -105,8 +105,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$this->initializeUri($config);
// Grav may return redirect response right away.
- if ($config->get('system.pages.redirect_trailing_slash', false)) {
- $response = $this->handleRedirectRequest($request);
+ $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
+ if ($redirectCode) {
+ $response = $this->handleRedirectRequest($request, $redirectCode > 300 ? $redirectCode : null);
if ($response) {
$this->stopTimer('_init');
@@ -413,7 +414,7 @@ protected function initializeUri(Config $config): void
$this->stopTimer('_init_uri');
}
- protected function handleRedirectRequest(RequestInterface $request): ?ResponseInterface
+ protected function handleRedirectRequest(RequestInterface $request, int $code = null): ?ResponseInterface
{
if (!in_array($request->getMethod(), ['GET', 'HEAD'])) {
return null;
@@ -426,7 +427,7 @@ protected function handleRedirectRequest(RequestInterface $request): ?ResponseIn
if ($path !== $root && $path !== $root . '/' && Utils::endsWith($path, '/')) {
// Use permanent redirect for SEO reasons.
- return $this->container->getRedirectResponse((string)$uri->withPath(rtrim($path, '/')), 301);
+ return $this->container->getRedirectResponse((string)$uri->withPath(rtrim($path, '/')), $code);
}
return null;
diff --git a/system/src/Grav/Common/Service/PagesServiceProvider.php b/system/src/Grav/Common/Service/PagesServiceProvider.php
index 4acf370..55f6a45 100644
--- a/system/src/Grav/Common/Service/PagesServiceProvider.php
+++ b/system/src/Grav/Common/Service/PagesServiceProvider.php
@@ -77,32 +77,46 @@ public function register(Container $container)
}
}
- $url = $pages->route($page->route());
+ $route = $page->route();
+ if ($route && \in_array($uri->method(), ['GET', 'HEAD'], true)) {
+ $pageExtension = $page->urlExtension();
+ $url = $pages->route($route) . $pageExtension;
+
+ if ($uri->params()) {
+ if ($url === '/') { //Avoid double slash
+ $url = $uri->params();
+ } else {
+ $url .= $uri->params();
+ }
+ }
+ if ($uri->query()) {
+ $url .= '?' . $uri->query();
+ }
+ if ($uri->fragment()) {
+ $url .= '#' . $uri->fragment();
+ }
+
+ /** @var Language $language */
+ $language = $grav['language'];
+
+ $redirectCode = (int)$config->get('system.pages.redirect_default_route', 0);
- if ($uri->params()) {
- if ($url === '/') { //Avoid double slash
- $url = $uri->params();
- } else {
- $url .= $uri->params();
+ // Language-specific redirection scenarios
+ if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
+ $grav->redirect($url, $redirectCode);
}
- }
- if ($uri->query()) {
- $url .= '?' . $uri->query();
- }
- if ($uri->fragment()) {
- $url .= '#' . $uri->fragment();
- }
- /** @var Language $language */
- $language = $grav['language'];
+ // Default route test and redirect
+ if ($redirectCode) {
+ $uriExtension = $uri->extension();
+ $uriExtension = null !== $uriExtension ? '.' . $uriExtension : '';
- // Language-specific redirection scenarios
- if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
- $grav->redirect($url);
- }
- // Default route test and redirect
- if ($config->get('system.pages.redirect_default_route') && $page->route() !== $path) {
- $grav->redirect($url);
+ if ($route !== $path || ($pageExtension !== $uriExtension
+ && \in_array($pageExtension, ['', '.htm', '.html'], true)
+ && \in_array($uriExtension, ['', '.htm', '.html'], true))) {
+ $grav->redirect($url, $redirectCode);
+ }
+ }
}
}
diff --git a/system/src/Grav/Common/Theme.php b/system/src/Grav/Common/Theme.php
index 2b807b6..a5006a2 100644
--- a/system/src/Grav/Common/Theme.php
+++ b/system/src/Grav/Common/Theme.php
@@ -9,9 +9,9 @@
namespace Grav\Common;
-use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Config\Config;
use RocketTheme\Toolbox\File\YamlFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Class Theme
@@ -44,53 +44,30 @@ public function config()
/**
* Persists to disk the theme parameters currently stored in the Grav Config object
*
- * @param string $theme_name The name of the theme whose config it should store.
+ * @param string $name The name of the theme whose config it should store.
* @return bool
*/
- public static function saveConfig($theme_name)
+ public static function saveConfig($name)
{
- if (!$theme_name) {
+ if (!$name) {
return false;
}
$grav = Grav::instance();
+
+ /** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $filename = 'config://themes/' . $theme_name . '.yaml';
- $file = YamlFile::instance($locator->findResource($filename, true, true));
- $content = $grav['config']->get('themes.' . $theme_name);
+
+ $filename = 'config://themes/' . $name . '.yaml';
+ $file = YamlFile::instance((string)$locator->findResource($filename, true, true));
+ $content = $grav['config']->get('themes.' . $name);
$file->save($content);
$file->free();
+ unset($file);
return true;
}
- /**
- * Override the mergeConfig method to work for themes
- *
- * @param PageInterface $page
- * @param string $deep
- * @param array $params
- * @param string $type
- * @return Data\Data
- */
- protected function mergeConfig(PageInterface $page, $deep = 'merge', $params = [], $type = 'themes')
- {
- return parent::mergeConfig($page, $deep, $params, $type);
- }
-
- /**
- * Simpler getter for the theme blueprint
- *
- * @return mixed
- */
- public function getBlueprint()
- {
- if (!$this->blueprint) {
- $this->loadBlueprint();
- }
- return $this->blueprint;
- }
-
/**
* Load blueprints.
*
@@ -100,8 +77,11 @@ protected function loadBlueprint()
{
if (!$this->blueprint) {
$grav = Grav::instance();
+ /** @var Themes $themes */
$themes = $grav['themes'];
- $this->blueprint = $themes->get($this->name)->blueprints();
+ $data = $themes->get($this->name);
+ \assert($data !== null);
+ $this->blueprint = $data->blueprints();
}
}
}
diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php
index 6adeea0..bf155ed 100644
--- a/system/src/Grav/Common/Themes.php
+++ b/system/src/Grav/Common/Themes.php
@@ -33,10 +33,8 @@ class Themes extends Iterator
{
/** @var Grav */
protected $grav;
-
/** @var Config */
protected $config;
-
/** @var bool */
protected $inited = false;
@@ -95,6 +93,20 @@ public function initTheme()
$events->addSubscriber($instance);
}
+ // Register blueprints.
+ if (is_dir('theme://blueprints/pages')) {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->grav['locator'];
+ $locator->addPath('blueprints', '', ['theme://blueprints'], ['user', 'blueprints']);
+ }
+
+ // Register form fields.
+ if (method_exists($instance, 'getFormFieldTypes')) {
+ /** @var Plugins $plugins */
+ $plugins = $this->grav['plugins'];
+ $plugins->formFieldTypes = $instance->getFormFieldTypes() + $plugins->formFieldTypes;
+ }
+
$this->grav['theme'] = $instance;
$this->grav->fireEvent('onThemeInitialized');
@@ -382,7 +394,10 @@ protected function autoloadTheme($class)
}
// Try Old style theme classes
- $path = strtolower(preg_replace('#\\\|_(?!.+\\\)#', '/', $class));
+ $path = preg_replace('#\\\|_(?!.+\\\)#', '/', $class);
+ \assert(null !== $path);
+
+ $path = strtolower($path);
$file = $locator("themes://{$path}/theme.php") ?: $locator("themes://{$path}/{$path}.php");
// Load class
diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php
index dd98b16..f704608 100644
--- a/system/src/Grav/Common/Uri.php
+++ b/system/src/Grav/Common/Uri.php
@@ -21,6 +21,7 @@
use RuntimeException;
use function array_key_exists;
use function count;
+use function in_array;
use function is_array;
use function is_string;
use function strlen;
@@ -394,7 +395,7 @@ public function path()
* Return the Extension of the URI
*
* @param string|null $default
- * @return string The extension of the URI
+ * @return string|null The extension of the URI
*/
public function extension($default = null)
{
@@ -518,7 +519,7 @@ public function basename()
* Return the full uri
*
* @param bool $include_root
- * @return mixed
+ * @return string
*/
public function uri($include_root = true)
{
@@ -1408,18 +1409,14 @@ public function getContentType($short = true)
/**
* Check if this is a valid Grav extension
*
- * @param string $extension
+ * @param string|null $extension
* @return bool
*/
- public function isValidExtension($extension)
+ public function isValidExtension($extension): bool
{
- $valid_page_types = implode('|', Utils::getSupportPageTypes());
+ $extension = (string)$extension;
- // Strip the file extension for valid page types
- if (preg_match('/(' . $valid_page_types . ')/', $extension)) {
- return true;
- }
- return false;
+ return $extension !== '' && in_array($extension, Utils::getSupportPageTypes(), true);
}
/**
diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php
index a076f0a..dac5cf7 100644
--- a/system/src/Grav/Common/Utils.php
+++ b/system/src/Grav/Common/Utils.php
@@ -12,11 +12,15 @@
use DateTime;
use DateTimeZone;
use Exception;
+use Grav\Common\Flex\Types\Pages\PageObject;
use Grav\Common\Helpers\Truncator;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Markdown\Excerpts;
+use Grav\Common\Page\Pages;
+use Grav\Framework\Flex\Flex;
+use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use InvalidArgumentException;
use Negotiation\Accept;
use Negotiation\Negotiator;
@@ -1520,7 +1524,7 @@ public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC
}
/**
- * Get path based on a token
+ * Get relative page path based on a token.
*
* @param string $path
* @param PageInterface|null $page
@@ -1529,47 +1533,122 @@ public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC
*/
public static function getPagePathFromToken($path, PageInterface $page = null)
{
- $path_parts = pathinfo($path);
- $grav = Grav::instance();
+ return static::getPathFromToken($path, $page);
+ }
- $basename = '';
- if (isset($path_parts['extension'])) {
- $basename = '/' . $path_parts['basename'];
- $path = rtrim($path_parts['dirname'], ':');
+ /**
+ * Get relative path based on a token.
+ *
+ * Path supports following syntaxes:
+ *
+ * 'self@', 'self@/path'
+ * 'page@:/route', 'page@:/route/filename.ext'
+ * 'theme@:', 'theme@:/path'
+ *
+ * @param string $path
+ * @param FlexObjectInterface|PageInterface|null $object
+ * @return string
+ * @throws RuntimeException
+ */
+ public static function getPathFromToken($path, $object = null)
+ {
+ $matches = static::resolveTokenPath($path);
+ if (null === $matches) {
+ return $path;
}
- $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
- preg_match($regex, $path, $matches);
+ $grav = Grav::instance();
- if ($matches) {
- if ($matches[1]) {
- if (null === $page) {
- throw new RuntimeException('Page not available for this self@ reference');
+ switch ($matches[0]) {
+ case 'self':
+ if (null === $object) {
+ throw new RuntimeException(sprintf('Page not available for self@ reference: %s', $path));
}
- } elseif ($matches[2]) {
- // page@
- $parts = explode(':', $path);
- $route = $parts[1];
- $page = $grav['page']->find($route);
- } elseif ($matches[3]) {
- // theme@
- $parts = explode(':', $path);
- $route = $parts[1];
- $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource('theme://'));
-
- return $theme . $route . $basename;
- }
- } else {
- return $path . $basename;
- }
- if (!$page) {
- throw new RuntimeException('Page route not found: ' . $path);
+ if ($matches[2] === '') {
+ if ($object->exists()) {
+ $route = '/' . $matches[1];
+
+ if ($object instanceof PageInterface) {
+ return trim($object->relativePagePath() . $route, '/');
+ }
+
+ $folder = $object->getMediaFolder();
+ if ($folder) {
+ return trim($folder . $route, '/');
+ }
+ } else {
+ return '';
+ }
+ }
+
+ break;
+ case 'page':
+ if ($matches[1] === '') {
+ $route = '/' . $matches[2];
+
+ // Exclude filename from the page lookup.
+ if (pathinfo($route, PATHINFO_EXTENSION)) {
+ $basename = '/' . basename($route);
+ $route = \dirname($route);
+ } else {
+ $basename = '';
+ }
+
+ $key = trim($route === '/' ? $grav['config']->get('system.home.alias') : $route, '/');
+ if ($object instanceof PageObject) {
+ $object = $object->getFlexDirectory()->getObject($key);
+ } elseif (static::isAdminPlugin()) {
+ /** @var Flex|null $flex */
+ $flex = $grav['flex'] ?? null;
+ $object = $flex ? $flex->getObject($key, 'pages') : null;
+ } else {
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
+ $object = $pages->find($route);
+ }
+
+ if ($object instanceof PageInterface) {
+ return trim($object->relativePagePath() . $basename, '/');
+ }
+ }
+
+ break;
+ case 'theme':
+ if ($matches[1] === '') {
+ $route = '/' . $matches[2];
+ $theme = $grav['locator']->findResource('theme://', false);
+ if (false !== $theme) {
+ return trim($theme . $route, '/');
+ }
+ }
+
+ break;
}
- $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
+ throw new RuntimeException(sprintf('Token path not found: %s', $path));
+ }
+
+ /**
+ * Returns [token, route, path] from '@token/route:/path'. Route and path are optional. If pattern does not match, return null.
+ *
+ * @param string $path
+ * @return string[]|null
+ */
+ private static function resolveTokenPath(string $path): ?array
+ {
+ if (strpos($path, '@') !== false) {
+ $regex = '/^(@\w+|\w+@|@\w+@)([^:]*)(.*)$/u';
+ if (preg_match($regex, $path, $matches)) {
+ return [
+ trim($matches[1], '@'),
+ trim($matches[2], '/'),
+ trim($matches[3], ':/')
+ ];
+ }
+ }
- return $path . $basename;
+ return null;
}
/**
diff --git a/system/src/Grav/Console/Cli/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php
index 8aa019c..b0c9499 100644
--- a/system/src/Grav/Console/Cli/CleanCommand.php
+++ b/system/src/Grav/Console/Cli/CleanCommand.php
@@ -154,8 +154,6 @@ class CleanCommand extends Command
'vendor/itsgoingd/clockwork/.gitattributes',
'vendor/itsgoingd/clockwork/CHANGELOG.md',
'vendor/itsgoingd/clockwork/composer.json',
- 'vendor/kodus/psr7-server/composer.json',
- 'vendor/kodus/psr7-server/CHANGELOG.md',
'vendor/league/climate/composer.json',
'vendor/league/climate/CHANGELOG.md',
'vendor/league/climate/CONTRIBUTING.md',
@@ -197,6 +195,9 @@ class CleanCommand extends Command
'vendor/nyholm/psr7/phpstan.neon.dist',
'vendor/nyholm/psr7/CHANGELOG.md',
'vendor/nyholm/psr7/psalm.xml',
+ 'vendor/nyholm/psr7-server/.github',
+ 'vendor/nyholm/psr7-server/composer.json',
+ 'vendor/nyholm/psr7-server/CHANGELOG.md',
'vendor/phive/twig-extensions-deferred/.gitignore',
'vendor/phive/twig-extensions-deferred/.travis.yml',
'vendor/phive/twig-extensions-deferred/composer.json',
diff --git a/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
index 1c9181b..bbc8f1a 100644
--- a/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
+++ b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
@@ -14,11 +14,13 @@
use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Grav;
+use Grav\Common\Utils;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Framework\Route\Route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
use Throwable;
use function get_class;
use function in_array;
@@ -76,6 +78,55 @@ protected function createJsonResponse(array $content, int $code = null, array $h
return new Response($code, $headers, json_encode($content));
}
+ /**
+ * @param string $filename
+ * @param string|resource|StreamInterface $resource
+ * @param array|null $headers
+ * @param array|null $options
+ * @return ResponseInterface
+ */
+ protected function createDownloadResponse(string $filename, $resource, array $headers = null, array $options = null): ResponseInterface
+ {
+ // Required for IE, otherwise Content-Disposition may be ignored
+ if (ini_get('zlib.output_compression')) {
+ @ini_set('zlib.output_compression', 'Off');
+ }
+
+ $headers = $headers ?? [];
+ $options = $options ?? ['force_download' => true];
+
+ $file_parts = pathinfo($filename);
+
+ if (!isset($headers['Content-Type'])) {
+ $mimetype = Utils::getMimeByExtension($file_parts['extension']);
+
+ $headers['Content-Type'] = $mimetype;
+ }
+
+ // TODO: add multipart download support.
+ //$headers['Accept-Ranges'] = 'bytes';
+
+ if (!empty($options['force_download'])) {
+ $headers['Content-Disposition'] = 'attachment; filename="' . $file_parts['basename'] . '"';
+ }
+
+ if (!isset($headers['Content-Length'])) {
+ $realpath = realpath($filename);
+ if ($realpath) {
+ $headers['Content-Length'] = filesize($realpath);
+ }
+ }
+
+ $headers += [
+ 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
+ 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
+ 'Cache-Control' => 'no-store, no-cache, must-revalidate',
+ 'Pragma' => 'no-cache'
+ ];
+
+ return new Response(200, $headers, $resource);
+ }
+
/**
* @param string $url
* @param int|null $code
diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php
index 98f6c8b..d480bc9 100644
--- a/system/src/Grav/Framework/Flex/FlexIndex.php
+++ b/system/src/Grav/Framework/Flex/FlexIndex.php
@@ -380,7 +380,7 @@ public function orderBy(array $orderings)
// Handle primary key alias.
$keyField = $this->getFlexDirectory()->getStorage()->getKeyField();
- if (isset($orderings[$keyField])) {
+ if ($keyField !== 'key' && $keyField !== 'storage_key' && isset($orderings[$keyField])) {
$orderings['key'] = $orderings[$keyField];
unset($orderings[$keyField]);
}
diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php
index 9011745..09c5638 100644
--- a/system/src/Grav/Framework/Flex/FlexObject.php
+++ b/system/src/Grav/Framework/Flex/FlexObject.php
@@ -885,6 +885,14 @@ public function __debugInfo()
];
}
+ /**
+ * Clone object.
+ */
+ public function __clone()
+ {
+ // Allows future compatibility as parent::__clone() works.
+ }
+
protected function markAsCopy(): void
{
$meta = $this->getMetaData();
diff --git a/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
index 09f9e60..621dae0 100644
--- a/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
+++ b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
@@ -16,7 +16,6 @@
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Traits\PageFormTrait;
use Grav\Common\User\Interfaces\UserCollectionInterface;
-use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
@@ -50,6 +49,20 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
/** @var array|null */
protected $_reorder;
+ /** @var FlexPageObject|null */
+ protected $_original;
+
+ /**
+ * Clone page.
+ */
+ public function __clone()
+ {
+ parent::__clone();
+
+ if (isset($this->header)) {
+ $this->header = clone($this->header);
+ }
+ }
/**
* @return array
@@ -242,6 +255,32 @@ public function save($reorder = true)
return parent::save();
}
+ /**
+ * Gets the Page Unmodified (original) version of the page.
+ *
+ * Assumes that object has been cloned before modifying it.
+ *
+ * @return FlexPageObject|null The original version of the page.
+ */
+ public function getOriginal()
+ {
+ return $this->_original;
+ }
+
+ /**
+ * Store the Page Unmodified (original) version of the page.
+ *
+ * Can be called multiple times, only the first call matters.
+ *
+ * @return void
+ */
+ public function storeOriginal(): void
+ {
+ if (null === $this->_original) {
+ $this->_original = clone $this;
+ }
+ }
+
/**
* Get display order for the associated media.
*
@@ -398,23 +437,6 @@ protected function filterElements(array &$elements, bool $extended = false): voi
unset($elements['content']);
}
- // TODO: Remove: RAW frontmatter support has been moved to Flex-Objects v1.0.2 controller.
- if (isset($elements['frontmatter'])) {
- $formatter = new YamlFormatter();
- try {
- // Replace the whole header except for media order, which is used in admin.
- $media_order = $elements['media_order'] ?? null;
- $elements['header'] = $formatter->decode($elements['frontmatter']);
- if ($media_order) {
- $elements['header']['media_order'] = $media_order;
- }
- } catch (RuntimeException $e) {
- throw new RuntimeException('Badly formatted markdown');
- }
-
- unset($elements['frontmatter']);
- }
-
if (!$extended) {
$folder = !empty($elements['folder']) ? trim($elements['folder']) : '';
diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
index d7af40f..5d3e968 100644
--- a/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
+++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
@@ -27,6 +27,8 @@ trait PageAuthorsTrait
{
/** @var array */
private $_authors;
+ /** @var array|null */
+ private $_permissionsCache;
/**
* Returns true if object has the named author.
@@ -70,15 +72,19 @@ public function getAuthors(): array
*/
public function getPermissions(bool $inherit = false)
{
- $permissions = [];
- if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) {
- $parent = $this->parent();
- if ($parent && method_exists($parent, 'getPermissions')) {
- $permissions = $parent->getPermissions($inherit);
+ if (null === $this->_permissionsCache) {
+ $permissions = [];
+ if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) {
+ $parent = $this->parent();
+ if ($parent && method_exists($parent, 'getPermissions')) {
+ $permissions = $parent->getPermissions($inherit);
+ }
}
+
+ $this->_permissionsCache = $this->loadPermissions($permissions);
}
- return $this->loadPermissions($permissions);
+ return $this->_permissionsCache;
}
/**
diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
index ba81d8f..1c32bf5 100644
--- a/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
+++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
@@ -277,6 +277,8 @@ public function move(PageInterface $parent)
throw new RuntimeException('Failed: Cannot set page parent to a child of current page');
}
+ $this->storeOriginal();
+
// TODO:
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
@@ -292,6 +294,8 @@ public function move(PageInterface $parent)
*/
public function copy(PageInterface $parent = null)
{
+ $this->storeOriginal();
+
$filesystem = Filesystem::getInstance(false);
$parentStorageKey = ltrim($filesystem->dirname("/{$this->getMasterKey()}"), '/');
@@ -715,8 +719,9 @@ public function filePath($var = null): ?string
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
+ $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
- return $locator->findResource($folder, true, true) . '/' . ($this->isPage() ? $this->name() : 'default.md');
+ return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md');
}
/**
@@ -733,8 +738,9 @@ public function filePathClean(): ?string
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
+ $folder = $locator->isStream($folder) ? $locator->getResource($folder, false) : $folder;
- return $locator->findResource($folder, false, true) . '/' . ($this->isPage() ? $this->name() : 'default.md');
+ return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md');
}
/**
@@ -1085,18 +1091,6 @@ public function folderExists(): bool
return $this->exists() || is_dir($this->getStorageFolder() ?? '');
}
- /**
- * Gets the Page Unmodified (original) version of the page.
- *
- * Assumes that object has been cloned before modifying it.
- *
- * @return PageInterface|null The original version of the page.
- */
- public function getOriginal()
- {
- return $this->getFlexDirectory()->getObject($this->getKey());
- }
-
/**
* Gets the action.
*
diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php
index 0029739..90773cd 100644
--- a/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php
+++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php
@@ -18,6 +18,7 @@
use Grav\Framework\Filesystem\Filesystem;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
+use function dirname;
use function is_string;
/**
@@ -32,6 +33,8 @@ trait PageRoutableTrait
private $_route;
/** @var string|null */
private $_path;
+ /** @var PageInterface|null */
+ private $_parentCache;
/**
* Returns the page extension, got from the page `url_extension` config and falls back to the
@@ -317,7 +320,7 @@ public function relativePagePath(): ?string
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $path = $locator->findResource($folder, false);
+ $path = $locator->isStream($folder) ? $locator->findResource($folder, false) : $folder;
return is_string($path) ? $path : null;
}
@@ -350,7 +353,7 @@ public function path($var = null): ?string
if ($folder) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $folder = $locator($folder);
+ $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
}
return $this->_path = is_string($folder) ? $folder : null;
@@ -413,26 +416,29 @@ public function parent(PageInterface $var = null)
throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
}
- if ($this->root()) {
- return null;
+ if ($this->_parentCache || $this->root()) {
+ return $this->_parentCache;
}
+ // Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'.
$filesystem = Filesystem::getInstance(false);
$directory = $this->getFlexDirectory();
$parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/');
- if ($parentKey) {
+ if ('' !== $parentKey) {
$parent = $directory->getObject($parentKey);
$language = $this->getLanguage();
if ($language && $parent && method_exists($parent, 'getTranslation')) {
$parent = $parent->getTranslation($language) ?? $parent;
}
- return $parent;
- }
+ $this->_parentCache = $parent;
+ } else {
+ $index = $directory->getIndex();
- $index = $directory->getIndex();
+ $this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null;
+ }
- return method_exists($index, 'getRoot') ? $index->getRoot() : null;
+ return $this->_parentCache;
}
/**
@@ -493,22 +499,22 @@ public function active(): bool
public function activeChild(): bool
{
$grav = Grav::instance();
+ /** @var Uri $uri */
$uri = $grav['uri'];
+ /** @var Pages $pages */
$pages = $grav['pages'];
$uri_path = rtrim(urldecode($uri->path()), '/');
$routes = $pages->routes();
if (isset($routes[$uri_path])) {
- /** @var PageInterface $child_page|null */
- $child_page = $pages->find($uri->route())->parent();
- if (null !== $child_page) {
- while (!$child_page->root()) {
- if ($this->path() === $child_page->path()) {
- return true;
- }
- /** @var PageInterface $child_page|null */
- $child_page = $child_page->parent();
+ $page = $pages->find($uri->route());
+ /** @var PageInterface|null $child_page */
+ $child_page = $page ? $page->parent() : null;
+ while ($child_page && !$child_page->root()) {
+ if ($this->path() === $child_page->path()) {
+ return true;
}
+ $child_page = $child_page->parent();
}
}
diff --git a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php
index 97384ab..1934f50 100644
--- a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php
+++ b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php
@@ -186,10 +186,10 @@ protected function resolvePath(string $path): string
$locator = Grav::instance()['locator'];
if (!$locator->isStream($path)) {
- return $path;
+ return GRAV_ROOT . "/{$path}";
}
- return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true));
+ return $locator->getResource($path);
}
/**
diff --git a/system/src/Grav/Framework/Flex/Storage/FileStorage.php b/system/src/Grav/Framework/Flex/Storage/FileStorage.php
index 2dc0757..eabd658 100644
--- a/system/src/Grav/Framework/Flex/Storage/FileStorage.php
+++ b/system/src/Grav/Framework/Flex/Storage/FileStorage.php
@@ -83,6 +83,8 @@ public function renameRow(string $src, string $dst): bool
$path = $this->getPathFromKey($src);
$file = $this->getFile($path);
$file->delete();
+ $file->free();
+ unset($file);
return true;
}
diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php
index acdab78..229194d 100644
--- a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php
+++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php
@@ -377,12 +377,14 @@ protected function loadRow(string $key): ?array
$file = $this->getFile($path);
try {
$data = (array)$file->content();
- $file->free();
if (isset($data[0])) {
throw new RuntimeException('Broken object file');
}
} catch (RuntimeException $e) {
$data = ['__ERROR' => $e->getMessage()];
+ } finally {
+ $file->free();
+ unset($file);
}
$data['__META'] = $this->getObjectMeta($key);
@@ -426,13 +428,17 @@ protected function saveRow(string $key, array $row): array
$file->save($row);
+ } catch (RuntimeException $e) {
+ throw new RuntimeException(sprintf('Flex saveFile(%s): %s', $path ?? $key, $e->getMessage()));
+ } finally {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- if ($locator->isStream($path)) {
- $locator->clearCache();
+ $locator->clearCache();
+
+ if (isset($file)) {
+ $file->free();
+ unset($file);
}
- } catch (RuntimeException $e) {
- throw new RuntimeException(sprintf('Flex saveFile(%s): %s', $path ?? $key, $e->getMessage()));
}
$row['__META'] = $this->getObjectMeta($key, true);
@@ -452,14 +458,14 @@ protected function deleteFile(File $file)
if ($file->exists()) {
$file->delete();
}
-
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($filename)) {
- $locator->clearCache($filename);
- }
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf('Flex deleteFile(%s): %s', $filename, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
+
+ $file->free();
}
return $data;
@@ -474,14 +480,12 @@ protected function copyFolder(string $src, string $dst): bool
{
try {
Folder::copy($this->resolvePath($src), $this->resolvePath($dst));
-
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($src) || $locator->isStream($dst)) {
- $locator->clearCache();
- }
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf('Flex copyFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
}
return true;
@@ -496,14 +500,12 @@ protected function moveFolder(string $src, string $dst): bool
{
try {
Folder::move($this->resolvePath($src), $this->resolvePath($dst));
-
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($src) || $locator->isStream($dst)) {
- $locator->clearCache();
- }
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
}
return true;
@@ -517,17 +519,13 @@ protected function moveFolder(string $src, string $dst): bool
protected function deleteFolder(string $path, bool $include_target = false): bool
{
try {
- $success = Folder::delete($this->resolvePath($path), $include_target);
-
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($path)) {
- $locator->clearCache();
- }
-
- return $success;
+ return Folder::delete($this->resolvePath($path), $include_target);
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
}
}
@@ -669,7 +667,14 @@ protected function initOptions(array $options): void
/** @var string $pattern */
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
- $this->dataFolder = $options['folder'];
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $folder = $options['folder'];
+ if ($locator->isStream($folder)) {
+ $folder = $locator->getResource($folder, false);
+ }
+
+ $this->dataFolder = $folder;
$this->dataFile = $options['file'] ?? 'item';
$this->dataExt = $extension;
if (mb_strpos($pattern, '{FILE}') === false && mb_strpos($pattern, '{EXT}') === false) {
diff --git a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
index df889ce..85c1429 100644
--- a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
+++ b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
@@ -414,9 +414,13 @@ protected function save(): void
}
$file->save($content);
$this->modified = (int)$file->modified(); // cast false to 0
- $file->free();
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf('Flex save(): %s', $e->getMessage()));
+ } finally {
+ if (isset($file)) {
+ $file->free();
+ unset($file);
+ }
}
}
@@ -453,6 +457,10 @@ protected function buildIndex(): array
$data = new Data($content);
$content = $data->get($this->prefix);
}
+
+ $file->free();
+ unset($file);
+
$this->data = $content;
$list = [];
diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
index c03a23d..c9ae580 100644
--- a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
+++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
@@ -14,8 +14,10 @@
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaUploadInterface;
use Grav\Common\Media\Traits\MediaTrait;
+use Grav\Common\Page\Media;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
+use Grav\Common\Utils;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\FlexDirectory;
@@ -23,6 +25,7 @@
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
+use function array_key_exists;
use function in_array;
use function is_array;
use function is_callable;
@@ -75,11 +78,40 @@ public function getMedia()
return $media;
}
+ /**
+ * @param string $field
+ * @return MediaCollectionInterface|null
+ */
+ public function getMediaField(string $field): ?MediaCollectionInterface
+ {
+ // Field specific media.
+ $settings = $this->getFieldSettings($field);
+ if (!empty($settings['media_field'])) {
+ $var = 'destination';
+ } elseif (!empty($settings['media_picker_field'])) {
+ $var = 'folder';
+ }
+
+ if (empty($var)) {
+ // Not a media field.
+ $media = null;
+ } elseif ($settings['self']) {
+ // Uses main media.
+ $media = $this->getMedia();
+ } else {
+ // Uses custom media.
+ $media = new Media($settings[$var]);
+ $this->addUpdatedMedia($media);
+ }
+
+ return $media;
+ }
+
/**
* @param string $field
* @return array|null
*/
- protected function getFieldSettings(string $field): ?array
+ public function getFieldSettings(string $field): ?array
{
if ($field === '') {
return null;
@@ -88,14 +120,32 @@ protected function getFieldSettings(string $field): ?array
// Load settings for the field.
$schema = $this->getBlueprint()->schema();
$settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
+ if (!isset($settings) || !is_array($settings)) {
+ return null;
+ }
- if (isset($settings['type']) && (in_array($settings['type'], ['avatar', 'file', 'pagemedia']) || !empty($settings['destination']))) {
- // Set destination folder.
+ $type = $settings['type'] ?? '';
+
+ // Media field.
+ if (!empty($settings['media_field']) || array_key_exists('destination', $settings) || in_array($type, ['avatar', 'file', 'pagemedia'], true)) {
$settings['media_field'] = true;
- if (empty($settings['destination']) || in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) {
- $settings['destination'] = $this->getMediaFolder();
+ $var = 'destination';
+ }
+
+ // Media picker field.
+ if (!empty($settings['media_picker_field']) || in_array($type, ['filepicker', 'pagemediaselect'], true)) {
+ $settings['media_picker_field'] = true;
+ $var = 'folder';
+ }
+
+ // Set media folder for media fields.
+ if (isset($var)) {
+ $folder = $settings[$var] ?? '';
+ if (in_array(rtrim($folder, '/'), ['', '@self', 'self@', '@self@'], true)) {
+ $settings[$var] = $this->getMediaFolder();
$settings['self'] = true;
} else {
+ $settings[$var] = Utils::getPathFromToken($folder, $this);
$settings['self'] = false;
}
}
@@ -115,7 +165,6 @@ protected function getMediaFieldSettings(string $field): array
return $settings + ['accept' => '*', 'limit' => 1000, 'self' => true];
}
-
protected function getMediaFields(): array
{
// Load settings for the field.
@@ -206,12 +255,13 @@ public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile, stri
*/
public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null): void
{
- $media = $this->getMedia();
+ $settings = $this->getMediaFieldSettings($field ?? '');
+
+ $media = $field ? $this->getMediaField($field) : $this->getMedia();
if (!$media instanceof MediaUploadInterface) {
throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads.");
}
- $settings = $this->getMediaFieldSettings($field ?? '');
$filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
$media->copyUploadedFile($uploadedFile, $filename, $settings);
$this->clearMediaCache();
@@ -322,13 +372,20 @@ protected function addUpdatedMedia(MediaCollectionInterface $media): void
foreach ($this->getUpdatedMedia() as $filename => $upload) {
if (is_array($upload)) {
// Uses new format with [UploadedFileInterface, array].
- $upload = $upload[0];
+ $settings = $upload[1];
+ if ($settings['destination'] === $media->getPath()) {
+ $upload = $upload[0];
+ } else {
+ $upload = false;
+ }
}
- if ($upload) {
- $medium = MediumFactory::fromUploadedFile($upload);
+ if (false !== $upload) {
+ $medium = $upload ? MediumFactory::fromUploadedFile($upload) : null;
+ $updated = true;
if ($medium) {
- $updated = true;
$media->add($filename, $medium);
+ } else {
+ $media->hide($filename);
}
}
}
@@ -356,7 +413,6 @@ protected function saveUpdatedMedia(): void
return;
}
-
// Upload/delete altered files.
/**
* @var string $filename