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 'renderAttributes() . ">\n" . trim($buffer) . "\n\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 = 'renderAttributes(). ">\n" . $buffer . "\n\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