فهرست منبع

test appel img twig

ouidade 3 سال پیش
والد
کامیت
80be13743f
63فایلهای تغییر یافته به همراه979 افزوده شده و 1148 حذف شده
  1. 0 66
      .github/workflows/build.yaml
  2. 0 72
      .github/workflows/tests.yaml
  3. 29 1
      CHANGELOG.md
  4. 130 21
      README.md
  5. 7 1
      SECURITY.md
  6. 5 3
      composer.json
  7. 181 168
      composer.lock
  8. 39 20
      system/blueprints/config/system.yaml
  9. 1 1
      system/blueprints/flex/pages.yaml
  10. 4 3
      system/config/system.yaml
  11. 1 1
      system/defines.php
  12. 26 0
      system/src/Grav/Common/Assets/BaseAsset.php
  13. 1 1
      system/src/Grav/Common/Assets/Css.php
  14. 1 1
      system/src/Grav/Common/Assets/Js.php
  15. 3 2
      system/src/Grav/Common/Assets/Pipeline.php
  16. 4 2
      system/src/Grav/Common/Cache.php
  17. 40 7
      system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
  18. 42 10
      system/src/Grav/Common/Flex/Types/Pages/PageObject.php
  19. 21 8
      system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
  20. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
  21. 12 6
      system/src/Grav/Common/Grav.php
  22. 4 4
      system/src/Grav/Common/Helpers/LogViewer.php
  23. 1 1
      system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
  24. 2 2
      system/src/Grav/Common/Page/Markdown/Excerpts.php
  25. 1 1
      system/src/Grav/Common/Page/Page.php
  26. 30 8
      system/src/Grav/Common/Page/Pages.php
  27. 21 13
      system/src/Grav/Common/Page/Types.php
  28. 53 34
      system/src/Grav/Common/Plugin.php
  29. 27 11
      system/src/Grav/Common/Plugins.php
  30. 5 4
      system/src/Grav/Common/Processors/InitializeProcessor.php
  31. 36 22
      system/src/Grav/Common/Service/PagesServiceProvider.php
  32. 15 35
      system/src/Grav/Common/Theme.php
  33. 18 3
      system/src/Grav/Common/Themes.php
  34. 6 9
      system/src/Grav/Common/Uri.php
  35. 3 2
      system/src/Grav/Console/Cli/CleanCommand.php
  36. 51 0
      system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
  37. 1 1
      system/src/Grav/Framework/Flex/FlexIndex.php
  38. 8 0
      system/src/Grav/Framework/Flex/FlexObject.php
  39. 40 18
      system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
  40. 12 6
      system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
  41. 8 14
      system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
  42. 14 10
      system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php
  43. 2 2
      system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php
  44. 2 0
      system/src/Grav/Framework/Flex/Storage/FileStorage.php
  45. 38 33
      system/src/Grav/Framework/Flex/Storage/FolderStorage.php
  46. 9 1
      system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
  47. 0 87
      tmp/forms/fg86637jgl2sbvcbloavimfam4/73f35c84f67d9518064f6783910db89a/index.yaml
  48. 0 29
      tmp/forms/fg86637jgl2sbvcbloavimfam4/ae2c7e0ffb4b4b1c4654412e6429a17c/index.yaml
  49. 0 50
      tmp/forms/fg86637jgl2sbvcbloavimfam4/b24e06058e6bf2c3e455378b84824a00/index.yaml
  50. 0 107
      tmp/forms/hajnagponl11621ppdikc5e6g4/c5c66e69061fcfcc38c77e522e153efe/index.yaml
  51. 0 53
      tmp/forms/m8co5qkkf12uur4tra88dbie5t/cdb3ae30b3dab66b186228ae772dd1a0/index.yaml
  52. 0 31
      tmp/forms/m8co5qkkf12uur4tra88dbie5t/e75346bd00d0b9954ac33c8fde35d73f/index.yaml
  53. 0 84
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/a44b13d8025f9fb13b03184c68bd181f/index.yaml
  54. 0 29
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/e2b1fd25908ccd5d1621269a6a54fa1b/index.yaml
  55. 0 31
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/f8b847bb606447afa42e342e90a112bf/index.yaml
  56. 0 3
      user/blueprints/pages/modular/programmes.yaml
  57. 3 3
      user/config/site.yaml
  58. 8 9
      user/config/system.yaml
  59. 0 0
      user/pages/01.home/02._programmes/europan_france.jpg
  60. 0 0
      user/pages/01.home/02._programmes/popsu.png
  61. 0 0
      user/pages/01.home/02._programmes/programmes.md
  62. 1 2
      user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md
  63. 12 1
      user/themes/epau-antimatter/templates/modular/programmes.html.twig

+ 0 - 66
.github/workflows/build.yaml

@@ -1,66 +0,0 @@
-name: Release Builds
-
-on:
-  release:
-    types: [published]
-
-jobs:
-  build:
-    if: "!github.event.release.prerelease"
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v2
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: 7.3
-          extensions: opcache, gd
-          coverage: none
-        env:
-          COMPOSER_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
-
-      - name: Install Dependencies
-        run: |
-          sudo apt-get -y update -qq  < /dev/null > /dev/null
-          sudo apt-get -y install -qq git zip < /dev/null > /dev/null
-
-      - name: Retrieval of Builder Scripts
-        run: |
-          # Real Grav URL
-          curl --silent -H "Authorization: token ${{ secrets.GLOBAL_TOKEN }}" -H "Accept: application/vnd.github.v3.raw" ${{ secrets.BUILD_SCRIPT_URL }} --output build-grav.sh
-
-          # Development Local URL
-          # curl ${{ secrets.BUILD_SCRIPT_URL }} --output build-grav.sh
-
-      - name: Grav Builder
-        run: |
-          bash ./build-grav.sh
-
-      - name: Upload Grav Release Assets
-        id: upload-release-asset
-        uses: alexellis/upload-assets@0.2.3
-        env:
-          GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
-        with:
-          asset_paths: '["./grav-dist/*.zip"]'
-
-  slack:
-    name: Slack
-    needs: build
-    runs-on: ubuntu-latest
-    if: always()
-    steps:
-      - uses: technote-space/workflow-conclusion-action@v2
-      - uses: 8398a7/action-slack@v3
-        with:
-          status: failure
-          fields: repo,message,author,action
-          icon_emoji: ':octocat:'
-          author_name: 'Github Action Build'
-          text: '🚚 Automated Build Failure'
-        env:
-          GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
-          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-        if: env.WORKFLOW_CONCLUSION == 'failure'

+ 0 - 72
.github/workflows/tests.yaml

@@ -1,72 +0,0 @@
-name: PHP Tests
-
-on:
-  push:
-    branches: [ develop ]
-  pull_request:
-    branches: [ develop ]
-
-jobs:
-
-  unit-tests:
-
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      matrix:
-        php: [ 8.0, 7.4, 7.3]
-        os: [ubuntu-latest]
-
-    steps:
-      - uses: actions/checkout@v2
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php }}
-          extensions: opcache, gd
-          coverage: none
-        env:
-          COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-#      - name: Update composer
-#        run: composer update
-#
-#      - name: Validate composer.json and composer.lock
-#        run: composer validate
-
-      - name: Get composer cache directory
-        id: composer-cache
-        run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
-      - name: Cache dependencies
-        uses: actions/cache@v2
-        with:
-          path: ${{ steps.composer-cache.outputs.dir }}
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: ${{ runner.os }}-composer-
-
-      - name: Install dependencies
-        run: composer install --prefer-dist --no-progress
-
-      - name: Run test suite
-        run: vendor/bin/codecept run
-
-#  slack:
-#      name: Slack
-#      needs: unit-tests
-#      runs-on: ubuntu-latest
-#      if: always()
-#      steps:
-#        - uses: technote-space/workflow-conclusion-action@v2
-#        - uses: 8398a7/action-slack@v3
-#          with:
-#             status: failure
-#             fields: repo,message,author,action
-#             icon_emoji: ':octocat:'
-#             author_name: 'Github Action Tests'
-#             text: '💥 Automated Test Failure'
-#          env:
-#            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-#            SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-#          if: env.WORKFLOW_CONCLUSION == 'failure'

+ 29 - 1
CHANGELOG.md

@@ -1,8 +1,36 @@
+# 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

+ 130 - 21
README.md

@@ -1,41 +1,150 @@
+# ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
 
-# EPAU REad ME
+[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
+[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
+[![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org)
+ [![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors)
 
+Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform.  There is **Zero** installation required.  Just extract the ZIP archive, and you are already up and running.  It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
 
-  - intall grav avec composer
+The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
 
-    ```composer create-project getgrav/grav ~/webroot/grav```
+* [Twig Templating](https://twig.sensiolabs.org/): for powerful control of the user interface
+* [Markdown](https://en.wikipedia.org/wiki/Markdown): for easy content creation
+* [YAML](https://yaml.org): for simple configuration
+* [Parsedown](https://parsedown.org/): for fast Markdown and Markdown Extra support
+* [Doctrine Cache](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html): layer for performance
+* [Pimple Dependency Injection Container](https://pimple.sensiolabs.org/): for extensibility and maintainability
+* [Symfony Event Dispatcher](https://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling
+* [Symfony Console](https://symfony.com/doc/current/components/console/introduction.html): for CLI interface
+* [Gregwar Image Library](https://github.com/Gregwar/Image): for dynamic image manipulation
 
-  - lance serveur local
+# Requirements
 
-    ```cd ~/webroot/nomdusite```
-    ```php -S localhost:8000 system/router.php```
+- PHP 7.3.6 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
+- Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
 
-    Fenêtre à laisser ouverte!
+# Documentation
 
+The full documentation can be found from [learn.getgrav.org](https://learn.getgrav.org).
 
-  - dans autre fenetre, installer version admin
-    ```cd ~/webroot/nomdusite```
-    ```bin/gpm selfupgrade```  (mise à jours)
+# QuickStart
 
-    ```bin/gpm install admin```   (installe l’admin)
+These are the options to get Grav:
 
-    ```bin/plugin login newuser```   (créer un nouvel utilisateur)
+### Downloading a Grav Package
 
+You can download a **ready-built** package from the [Downloads page on https://getgrav.org](https://getgrav.org/downloads)
 
+### With Composer
 
-# Pour lancer le site
+You can create a new project with the latest **stable** Grav release with the following command:
 
-    ```cd ~/chemin/dudossier/dev-epau.archi.fr```
+```
+$ composer create-project getgrav/grav ~/webroot/grav
+```
 
-    pour premier lancement
+### From GitHub
 
-      ```php bin/grav install```
+1. Clone the Grav repository from [https://github.com/getgrav/grav]() to a folder in the webroot of your server, e.g. `~/webroot/grav`. Launch a **terminal** or **console** and navigate to the webroot folder:
+   ```
+   $ cd ~/webroot
+   $ git clone https://github.com/getgrav/grav.git
+   ```
 
-    ensuite
-      ```php -S localhost:8000 system/router.php```
-    Fenêtre à laisser ouverte!
+2. Install the **plugin** and **theme dependencies** by using the [Grav CLI application](https://learn.getgrav.org/advanced/grav-cli) `bin/grav`:
+   ```
+   $ cd ~/webroot/grav
+   $ bin/grav install
+   ```
 
-      ```bin/gpm install admin```   (installe l’admin)
+Check out the [install procedures](https://learn.getgrav.org/basics/installation) for more information.
 
-    récupérer les comptes
+# Adding Functionality
+
+You can download [plugins](https://getgrav.org/downloads/plugins) or [themes](https://getgrav.org/downloads/themes) manually from the appropriate tab on the [Downloads page on https://getgrav.org](https://getgrav.org/downloads), but the preferred solution is to use the [Grav Package Manager](https://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
+
+```
+$ bin/gpm index
+```
+
+This will display all the available plugins and then you can install one or more with:
+
+```
+$ bin/gpm install <plugin/theme>
+```
+
+# Updating
+
+To update Grav you should use the [Grav Package Manager](https://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
+
+```
+$ bin/gpm selfupgrade
+```
+
+To update plugins and themes:
+
+```
+$ bin/gpm update
+```
+
+## Upgrading from older version
+
+* [Upgrading to Grav 1.7](https://learn.getgrav.org/16/advanced/grav-development/grav-17-upgrade-guide)
+* [Upgrading to Grav 1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-16-upgrade-guide)
+* [Upgrading from Grav <1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-15-upgrade-guide)
+
+# Contributing
+We appreciate any contribution to Grav, whether it is related to bugs, grammar, or simply a suggestion or improvement! Please refer to the [Contributing guide](CONTRIBUTING.md) for more guidance on this topic.
+
+## Security issues
+If you discover a possible security issue related to Grav or one of its plugins, please email the core team at contact@getgrav.org and we'll address it as soon as possible.
+
+# Getting Started
+
+* [What is Grav?](https://learn.getgrav.org/basics/what-is-grav)
+* [Install](https://learn.getgrav.org/basics/installation) Grav in few seconds
+* Understand the [Configuration](https://learn.getgrav.org/basics/grav-configuration)
+* Take a peek at our available free [Skeletons](https://getgrav.org/downloads/skeletons)
+* If you have questions, jump on our [Discord Chat Server](https://chat.getgrav.org)!
+* Have fun!
+
+# Exploring More
+
+* Have a look at our [Basic Tutorial](https://learn.getgrav.org/basics/basic-tutorial)
+* Dive into more [advanced](https://learn.getgrav.org/advanced) functions
+* Learn about the [Grav CLI](https://learn.getgrav.org/cli-console/grav-cli)
+* Review examples in the [Grav Cookbook](https://learn.getgrav.org/cookbook)
+* More [Awesome Grav Stuff](https://github.com/getgrav/awesome-grav)
+
+# Backers
+Support Grav with a monthly donation to help us continue development. [[Become a backer](https://opencollective.com/grav#backer)]
+
+<img src="https://opencollective.com/grav/tiers/backers.svg?avatarHeight=36&width=600" />
+
+# Sponsors
+Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/grav#sponsor)]
+
+<img src="https://opencollective.com/grav/tiers/sponsors.svg?avatarHeight=36&width=600" />
+
+# License
+
+See [LICENSE](LICENSE.txt)
+
+
+[gitflow-model]: http://nvie.com/posts/a-successful-git-branching-model/
+[gitflow-extensions]: https://github.com/nvie/gitflow
+
+# Running Tests
+
+First install the dev dependencies by running `composer install` from the Grav root.
+
+Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
+Windows users should use the `composer test-windows` command.
+You can also run a single unit test file, e.g. `composer test tests/unit/Grav/Common/AssetsTest.php`
+
+To run phpstan tests, you should run:
+
+* `composer phpstan` for global tests
+* `composer phpstan-framework` for more strict tests
+* `composer phpstan-plugins` to test all installed plugins

+ 7 - 1
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.

+ 5 - 3
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": "*",

+ 181 - 168
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.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/donatj/PhpUserAgent.git",
-                "reference": "978e66786bc392a09b24b152a8a695dadd230e60"
+                "reference": "f9a521726b2ce4c5173281ceaab5a02c05b691ef"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/978e66786bc392a09b24b152a8a695dadd230e60",
-                "reference": "978e66786bc392a09b24b152a8a695dadd230e60",
+                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/f9a521726b2ce4c5173281ceaab5a02c05b691ef",
+                "reference": "f9a521726b2ce4c5173281ceaab5a02c05b691ef",
                 "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.3.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-02-18T04:30:49+00:00"
         },
         {
             "name": "dragonmantank/cron-expression",
@@ -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",
@@ -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": [],

+ 39 - 20
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

+ 1 - 1
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:

+ 4 - 3
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
 

+ 1 - 1
system/defines.php

@@ -8,7 +8,7 @@
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.7.7');
+define('GRAV_VERSION', '1.7.8');
 define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
 define('GRAV_TESTING', false);
 

+ 26 - 0
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 @@ abstract class BaseAsset extends PropertyObject
 
         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 '';
+    }
 
 
     /**

+ 1 - 1
system/src/Grav/Common/Assets/Css.php

@@ -47,6 +47,6 @@ class Css extends BaseAsset
             return "<style>\n" . trim($buffer) . "\n</style>\n";
         }
 
-        return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+        return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . ">\n";
     }
 }

+ 1 - 1
system/src/Grav/Common/Assets/Js.php

@@ -43,6 +43,6 @@ class Js extends BaseAsset
             return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
         }
 
-        return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+        return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . "></script>\n";
     }
 }

+ 3 - 2
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 @@ class Pipeline extends PropertyObject
             $output = "<style>\n" . $buffer . "\n</style>\n";
         } else {
             $this->asset = $relative_path;
-            $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+            $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n";
         }
 
         return $output;
@@ -211,7 +212,7 @@ class Pipeline extends PropertyObject
             $output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
         } else {
             $this->asset = $relative_path;
-            $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+            $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . "></script>\n";
         }
 
         return $output;

+ 4 - 2
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 @@ class Cache extends Getters
                     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 @@ class Cache extends Getters
                                 $anything = true;
                             }
                         } elseif (is_dir($file)) {
-                            if (Folder::delete($file)) {
+                            if (Folder::delete($file, false)) {
                                 $anything = true;
                             }
                         }

+ 40 - 7
system/src/Grav/Common/Flex/Types/Pages/PageIndex.php

@@ -21,13 +21,16 @@ use Grav\Common\Grav;
 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 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
             '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 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                 $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 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                         '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;
@@ -535,20 +567,21 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
 
     /**
      * @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';
         }
 

+ 42 - 10
system/src/Grav/Common/Flex/Types/Pages/PageObject.php

@@ -62,7 +62,6 @@ class PageObject extends FlexPageObject
 
     /** @var string Language code, eg: 'en' */
     protected $language;
-
     /** @var string File format, eg. 'md' */
     protected $format;
 
@@ -78,6 +77,7 @@ class PageObject extends FlexPageObject
             'path' => true,
             'full_order' => true,
             'filterBy' => true,
+            'translated' => false,
         ] + parent::getCachedMethods();
     }
 
@@ -92,6 +92,11 @@ class PageObject extends FlexPageObject
         }
     }
 
+    public function translated(): bool
+    {
+        return $this->translatedLanguages(true) ? true : false;
+    }
+
     /**
      * @param string|array $query
      * @return Route|null
@@ -223,7 +228,7 @@ class PageObject extends FlexPageObject
         }
 
         // Reorder siblings.
-        $siblings = is_array($reorder) ? $this->reorderSiblings($reorder) : [];
+        $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
 
         $data = $this->prepareStorage();
         unset($data['header']);
@@ -289,6 +294,9 @@ class PageObject extends FlexPageObject
             $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 +316,14 @@ class PageObject extends FlexPageObject
 
         $this->_reorder = [];
         $this->setProperty('parent_key', $parent->getStorageKey());
+        $this->storeOriginal();
 
         return $this;
     }
 
     /**
      * @param array $ordering
-     * @return PageCollection
+     * @return PageCollection|null
      */
     protected function reorderSiblings(array $ordering)
     {
@@ -324,17 +333,35 @@ class PageObject extends FlexPageObject
         $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 +405,9 @@ class PageObject extends FlexPageObject
                     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 +613,17 @@ class PageObject extends FlexPageObject
             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;
         }
 

+ 21 - 8
system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php

@@ -110,6 +110,9 @@ class PageStorage extends FolderStorage
             }
         } catch (RuntimeException $e) {
             $frontmatter = 'ERROR: ' . $e->getMessage();
+        } finally {
+            $file->free();
+            unset($file);
         }
 
         return $frontmatter;
@@ -127,6 +130,9 @@ class PageStorage extends FolderStorage
             $raw = $file->raw();
         } catch (RuntimeException $e) {
             $raw = 'ERROR: ' . $e->getMessage();
+        } finally {
+            $file->free();
+            unset($file);
         }
 
         return $raw;
@@ -407,7 +413,7 @@ class PageStorage extends FolderStorage
                     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 @@ class PageStorage extends FolderStorage
             } 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 @@ class PageStorage extends FolderStorage
             $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;
             }

+ 1 - 1
system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php

@@ -67,7 +67,7 @@ trait PageTranslateTrait
         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) {

+ 12 - 6
system/src/Grav/Common/Grav.php

@@ -426,12 +426,18 @@ class Grav extends Container
         // 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 (null !== $code || $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) {

+ 4 - 4
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 @@ class LogViewer
     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;

+ 1 - 1
system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php

@@ -63,7 +63,7 @@ interface PageRoutableInterface
      * 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);
 

+ 2 - 2
system/src/Grav/Common/Page/Markdown/Excerpts.php

@@ -278,10 +278,10 @@ class Excerpts
             );
         }
 
-        $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,

+ 1 - 1
system/src/Grav/Common/Page/Page.php

@@ -1875,7 +1875,7 @@ class Page implements PageInterface
      * 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)
     {

+ 30 - 8
system/src/Grav/Common/Page/Pages.php

@@ -769,6 +769,9 @@ class Pages
     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 @@ class Pages
 
         $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();
                 }
@@ -1159,7 +1174,14 @@ class Pages
                 $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();

+ 21 - 13
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 @@ class Types implements \ArrayAccess, \Iterator, \Countable
         }
     }
 
+    /**
+     * @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 @@ class Types implements \ArrayAccess, \Iterator, \Countable
             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 @@ class Types implements \ArrayAccess, \Iterator, \Countable
 
     /**
      * @param string $uri
+     * @return void
      */
     public function scanTemplates($uri)
     {

+ 53 - 34
system/src/Grav/Common/Plugin.php

@@ -16,6 +16,7 @@ use Grav\Common\Page\Interfaces\PageInterface;
 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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
     /**
      * @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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
     /**
      * 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 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
      */
     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();
         }
     }
 }

+ 27 - 11
system/src/Grav/Common/Plugins.php

@@ -17,6 +17,7 @@ use Grav\Common\File\CompiledYamlFile;
 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 @@ use function is_object;
  */
 class Plugins extends Iterator
 {
-    /** @var array */
+    /** @var array|null */
     public $formFieldTypes;
 
     /** @var bool */
@@ -46,6 +47,7 @@ class Plugins extends Iterator
         $iterator = $locator->getIterator('plugins://');
 
         $plugins = [];
+        /** @var SplFileInfo $directory */
         foreach ($iterator as $directory) {
             if (!$directory->isDir()) {
                 continue;
@@ -56,7 +58,10 @@ class Plugins extends Iterator
         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 @@ class Plugins extends Iterator
         $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 @@ class Plugins extends Iterator
             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 @@ class Plugins extends Iterator
      * Add a plugin
      *
      * @param Plugin $plugin
+     * @return void
      */
     public function add($plugin)
     {
@@ -175,8 +189,8 @@ class Plugins extends Iterator
      */
     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 @@ class Plugins extends Iterator
     /**
      * Return list of all plugin data with their blueprints.
      *
-     * @return array<string,Data>
+     * @return Data[]
      */
     public static function all()
     {
         $grav = Grav::instance();
+
+        /** @var Plugins $plugins */
         $plugins = $grav['plugins'];
         $list = [];
 

+ 5 - 4
system/src/Grav/Common/Processors/InitializeProcessor.php

@@ -105,8 +105,9 @@ class InitializeProcessor extends ProcessorBase
         $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 @@ class InitializeProcessor extends ProcessorBase
         $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 @@ class InitializeProcessor extends ProcessorBase
 
         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;

+ 36 - 22
system/src/Grav/Common/Service/PagesServiceProvider.php

@@ -77,32 +77,46 @@ class PagesServiceProvider implements ServiceProviderInterface
                     }
                 }
 
-                $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);
+                        }
+                    }
                 }
             }
 

+ 15 - 35
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 @@ class Theme extends Plugin
     /**
      * 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 @@ class Theme extends Plugin
     {
         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();
         }
     }
 }

+ 18 - 3
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 @@ class Themes extends Iterator
                 $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 @@ class Themes extends Iterator
             }
 
             // 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

+ 6 - 9
system/src/Grav/Common/Uri.php

@@ -21,6 +21,7 @@ use RocketTheme\Toolbox\Event\Event;
 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 @@ class Uri
      * 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)
     {
@@ -1408,18 +1409,14 @@ class Uri
     /**
      * 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);
     }
 
     /**

+ 3 - 2
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',

+ 51 - 0
system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php

@@ -14,11 +14,13 @@ namespace Grav\Framework\Controller\Traits;
 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 @@ trait ControllerResponseTrait
         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

+ 1 - 1
system/src/Grav/Framework/Flex/FlexIndex.php

@@ -380,7 +380,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
 
         // 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]);
         }

+ 8 - 0
system/src/Grav/Framework/Flex/FlexObject.php

@@ -885,6 +885,14 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         ];
     }
 
+    /**
+     * Clone object.
+     */
+    public function __clone()
+    {
+        // Allows future compatibility as parent::__clone() works.
+    }
+
     protected function markAsCopy(): void
     {
         $meta = $this->getMetaData();

+ 40 - 18
system/src/Grav/Framework/Flex/Pages/FlexPageObject.php

@@ -16,7 +16,6 @@ use Grav\Common\Grav;
 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 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
         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 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
             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']) : '';
 

+ 12 - 6
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php

@@ -27,6 +27,8 @@ trait PageAuthorsTrait
 {
     /** @var array<int,UserInterface> */
     private $_authors;
+    /** @var array|null */
+    private $_permissionsCache;
 
     /**
      * Returns true if object has the named author.
@@ -70,15 +72,19 @@ trait PageAuthorsTrait
      */
     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;
     }
 
     /**

+ 8 - 14
system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php

@@ -277,6 +277,8 @@ trait PageLegacyTrait
             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 @@ trait PageLegacyTrait
      */
     public function copy(PageInterface $parent = null)
     {
+        $this->storeOriginal();
+
         $filesystem = Filesystem::getInstance(false);
 
         $parentStorageKey = ltrim($filesystem->dirname("/{$this->getMasterKey()}"), '/');
@@ -715,8 +719,9 @@ trait PageLegacyTrait
 
         /** @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 @@ trait PageLegacyTrait
 
         /** @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 @@ trait PageLegacyTrait
         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.
      *

+ 14 - 10
system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php

@@ -18,6 +18,7 @@ use Grav\Common\Uri;
 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 @@ trait PageRoutableTrait
 
         /** @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 @@ trait PageRoutableTrait
         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,13 +416,12 @@ trait PageRoutableTrait
             throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
         }
 
-        if ($this->root()) {
-            return null;
+        if ($this->_parentCache || $this->root()) {
+            return $this->_parentCache;
         }
 
-        $filesystem = Filesystem::getInstance(false);
         $directory = $this->getFlexDirectory();
-        $parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/');
+        $parentKey = ltrim(dirname("/{$this->getKey()}"), '/');
         if ($parentKey) {
             $parent = $directory->getObject($parentKey);
             $language = $this->getLanguage();
@@ -427,12 +429,14 @@ trait PageRoutableTrait
                 $parent = $parent->getTranslation($language) ?? $parent;
             }
 
-            return $parent;
-        }
+            $this->_parentCache = $parent;
+        } else {
+            $index = $directory->getIndex();
 
-        $index = $directory->getIndex();
+            $this->_parentCache = method_exists($index, 'getRoot') ? $index->getRoot() : null;
+        }
 
-        return method_exists($index, 'getRoot') ? $index->getRoot() : null;
+        return $this->_parentCache;
     }
 
     /**

+ 2 - 2
system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php

@@ -186,10 +186,10 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface
         $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);
     }
 
     /**

+ 2 - 0
system/src/Grav/Framework/Flex/Storage/FileStorage.php

@@ -83,6 +83,8 @@ class FileStorage extends FolderStorage
         $path = $this->getPathFromKey($src);
         $file = $this->getFile($path);
         $file->delete();
+        $file->free();
+        unset($file);
 
         return true;
     }

+ 38 - 33
system/src/Grav/Framework/Flex/Storage/FolderStorage.php

@@ -377,12 +377,14 @@ class FolderStorage extends AbstractFilesystemStorage
         $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 @@ class FolderStorage extends AbstractFilesystemStorage
 
             $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 @@ class FolderStorage extends AbstractFilesystemStorage
             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 @@ class FolderStorage extends AbstractFilesystemStorage
     {
         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 @@ class FolderStorage extends AbstractFilesystemStorage
     {
         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 @@ class FolderStorage extends AbstractFilesystemStorage
     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 @@ class FolderStorage extends AbstractFilesystemStorage
         /** @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) {

+ 9 - 1
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php

@@ -414,9 +414,13 @@ class SimpleStorage extends AbstractFilesystemStorage
             }
             $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 @@ class SimpleStorage extends AbstractFilesystemStorage
             $data = new Data($content);
             $content = $data->get($this->prefix);
         }
+
+        $file->free();
+        unset($file);
+
         $this->data = $content;
 
         $list = [];

+ 0 - 87
tmp/forms/fg86637jgl2sbvcbloavimfam4/73f35c84f67d9518064f6783910db89a/index.yaml

@@ -1,87 +0,0 @@
-form: flex-pages
-unique_id: 73f35c84f67d9518064f6783910db89a
-url: 'http://localhost:8000/admin/pages/gouvernance/conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1614952271
-  updated: 1614952271
-data:
-  header:
-    title: 'Membres du conseil d''administration'
-    media_order: null
-    body_classes: null
-    order_by: null
-    order_manual: null
-    published: null
-    date: null
-    publish_date: null
-    unpublish_date: null
-    metadata: null
-    taxonomy: null
-    dateformat: null
-    menu: null
-    slug: null
-    redirect: null
-    process: null
-    twig_first: null
-    never_cache_twig: null
-    child_type: null
-    routable: null
-    cache_enable: null
-    visible: null
-    debugger: null
-    template: null
-    append_url_extension: null
-    routes:
-      default: null
-      canonical: null
-      aliases: null
-    admin:
-      children_display_order: null
-    login:
-      visibility_requires_access: null
-    access: null
-    permissions:
-      inherit: null
-      authors: null
-      groups: null
-  content: "# Jean-Baptiste Butlen\r\nSous-directeur Aménagement\r\ndurable, Direction Générale de\r\nl’Aménagement, du Logement\r\net de la Nature, Ministère de la\r\nTransition écologique, Ministère\r\nde la Cohésion des territoires et\r\ndes Relations avec les\r\nCollectivités territoriales\r\n"
-  folder: membres-du-conseil-dadministration
-  route: /gouvernance/conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-  name: default
-  ordering: false
-  order: null
-  blueprint: null
-  lang: null
-  xss_check: null
-  value: null
-  _post_entries_save: null
-files: {  }
-object:
-  type: pages
-  key: gouvernance/conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-  storage_key: 03.gouvernance/01.conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-  timestamp: 1614952229
-  serialized:
-    __META:
-      key: gouvernance/conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-      storage_key: 03.gouvernance/01.conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-      template: default
-      storage_timestamp: 1614952229
-      markdown:
-        '': { default: 1614952229 }
-      checksum: 9a001ef28e8a20154a0f4f09a46ddebf
-    storage_key: 03.gouvernance/01.conseil-dadministration/presidente-du-conseil-dadministration/membres-du-conseil-dadministration
-    parent_key: 03.gouvernance/01.conseil-dadministration/presidente-du-conseil-dadministration
-    order: false
-    folder: membres-du-conseil-dadministration
-    template: default
-    lang: ''
-    header:
-      title: 'Membres du conseil d''administration'
-    root: false
-    markdown: "# Jean-Baptiste Butlen\nSous-directeur Aménagement\ndurable, Direction Générale de\nl’Aménagement, du Logement\net de la Nature, Ministère de la\nTransition écologique, Ministère\nde la Cohésion des territoires et\ndes Relations avec les\nCollectivités territoriales\n"
-    slug: membres-du-conseil-dadministration
-    name: default.md

+ 0 - 29
tmp/forms/fg86637jgl2sbvcbloavimfam4/ae2c7e0ffb4b4b1c4654412e6429a17c/index.yaml

@@ -1,29 +0,0 @@
-form: flex-pages
-unique_id: ae2c7e0ffb4b4b1c4654412e6429a17c
-url: '/pages/pr/:add'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1614944374
-  updated: 1614944374
-data: null
-files: {  }
-object:
-  type: pages
-  key: pr
-  storage_key: ''
-  timestamp: 0
-  serialized:
-    __META: {  }
-    storage_key: ''
-    parent_key: ''
-    order: '05.'
-    folder: pr
-    template: blog
-    lang: ''
-    root: false
-    route: /
-    name: blog
-    header:
-      title: Pr

+ 0 - 50
tmp/forms/fg86637jgl2sbvcbloavimfam4/b24e06058e6bf2c3e455378b84824a00/index.yaml

@@ -1,50 +0,0 @@
-form: flex-pages-raw
-unique_id: b24e06058e6bf2c3e455378b84824a00
-url: 'http://localhost:8000/admin/pages/gouvernance-2'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1614944089
-  updated: 1614944198
-data:
-  frontmatter: "---\r\ntitle: Gouvernance2\r\nmenu: GOUVERNANCE 2\r\nonpage_menu: true\r\nbody_classes: \"modular header-image fullwidth\"\r\n\r\ncontent:\r\n    items: '@self.modular'\r\n    order:\r\n        by: default\r\n        dir: asc\r\n        custom:\r\n---"
-  content: null
-  ordering: true
-  folder: gouvernance-2
-  route: null
-  name: modular
-  order: null
-  blueprint: null
-  xss_check: null
-  header:
-    media_order: null
-files: {  }
-object:
-  type: pages
-  key: gouvernance-2
-  storage_key: 04.gouvernance-2
-  timestamp: 1614943947
-  serialized:
-    __META:
-      key: gouvernance-2
-      storage_key: 04.gouvernance-2
-      template: modular
-      storage_timestamp: 1614943947
-      markdown:
-        '': { modular: 1614943947 }
-      checksum: 430b1700b40c2bea498e6dc4ba633605
-    storage_key: 04.gouvernance-2
-    parent_key: ''
-    order: '04.'
-    folder: gouvernance-2
-    template: modular
-    lang: ''
-    header:
-      title: Gouvernance-2
-      content:
-        items: '@self.modular'
-    root: false
-    markdown: ''
-    slug: gouvernance-2
-    name: modular.md

+ 0 - 107
tmp/forms/hajnagponl11621ppdikc5e6g4/c5c66e69061fcfcc38c77e522e153efe/index.yaml

@@ -1,107 +0,0 @@
-form: flex-pages
-unique_id: c5c66e69061fcfcc38c77e522e153efe
-url: 'http://localhost:8000/admin/pages/home/_gouvernance-module'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1615559247
-  updated: 1615559247
-data:
-  header:
-    title: Gouvernance
-    media_order: 'jeux-olypiques-paris-2024-village-athle-lot-e2.jpg,Logotype_GIP_EPAU.svg.png'
-    body_classes: 'modular fullwidth title-center title-h1h2'
-    menu: 'Gouvernance 2'
-    visible: true
-    debugger: true
-    admin:
-      children_display_order: collection
-    order_by: null
-    order_manual: null
-    onpage_menu: true
-    content:
-      items: '@self.modular'
-      order:
-        by: default
-        dir: asc
-        custom: [_conseil-dadministration_module]
-    published: null
-    date: null
-    publish_date: null
-    unpublish_date: null
-    metadata: null
-    taxonomy: null
-    dateformat: null
-    slug: null
-    redirect: null
-    process: null
-    twig_first: null
-    never_cache_twig: null
-    child_type: null
-    routable: null
-    cache_enable: null
-    template: null
-    append_url_extension: null
-    routes:
-      default: null
-      canonical: null
-      aliases: null
-    login:
-      visibility_requires_access: null
-    access: null
-    permissions:
-      inherit: null
-      authors: null
-      groups: null
-  content: '# Gouvernance'
-  folder: _gouvernance
-  route: /home
-  name: modular/gouvernance
-  ordering: true
-  order: '_accueil,_programmes,_rapports-dactivites,_gouvernance,_gouvernance,_contact'
-  blueprint: null
-  lang: null
-  xss_check: null
-  value: null
-  _post_entries_save: null
-files: {  }
-object:
-  type: pages
-  key: home/_gouvernance-module
-  storage_key: 01.home/06._gouvernance-module
-  timestamp: 1615558956
-  serialized:
-    __META:
-      key: home/_gouvernance-module
-      storage_key: 01.home/06._gouvernance-module
-      template: gouvernance
-      storage_timestamp: 1615558956
-      markdown:
-        '': { gouvernance: 1615558685 }
-      children:
-        01._conseil-dadministration_module: false
-      checksum: 7f21735022ad7faf780b47fe141b79e1
-    storage_key: 01.home/06._gouvernance-module
-    parent_key: 01.home
-    order: '06.'
-    folder: _gouvernance-module
-    template: gouvernance
-    lang: ''
-    header:
-      title: 'gouvernance module'
-      menu: 'gouvernance module'
-      visible: true
-      onpage_menu: true
-      body_classes: 'modular fullwidth title-center title-h1h2'
-      content:
-        items: '@self.modular'
-        order: { by: default, dir: asc, custom: [_conseil-dadministration_module] }
-      media_order: 'jeux-olypiques-paris-2024-village-athle-lot-e2.jpg,Logotype_GIP_EPAU.svg.png'
-      admin:
-        children_display_order: collection
-      debugger: true
-    root: false
-    markdown: "gouvernance module\n"
-    slug: _gouvernance-module
-    name: gouvernance.md

+ 0 - 53
tmp/forms/m8co5qkkf12uur4tra88dbie5t/cdb3ae30b3dab66b186228ae772dd1a0/index.yaml

@@ -1,53 +0,0 @@
-form: flex-pages-raw
-unique_id: cdb3ae30b3dab66b186228ae772dd1a0
-url: 'http://localhost:8000/admin/pages/programmes'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1614876264
-  updated: 1614876264
-data:
-  frontmatter: "title: Gouvernance\r\ncontent:\r\n  items: '@self.modular'\r\n"
-  content: null
-  ordering: true
-  folder: gouvernance
-  route: null
-  name: modular
-  order: 'home,gouvernance,gouvernance'
-  blueprint: null
-  xss_check: null
-  header:
-    media_order: null
-files: {  }
-object:
-  type: pages
-  key: programmes
-  storage_key: 03.programmes
-  timestamp: 1614875485
-  serialized:
-    __META:
-      key: programmes
-      storage_key: 03.programmes
-      template: modular
-      storage_timestamp: 1614875485
-      markdown:
-        '': { modular: 1614875485 }
-      children:
-        _european: false
-        _popsu: false
-      checksum: 17d79b5a489014a479adac578c6eb693
-    storage_key: 03.programmes
-    parent_key: ''
-    order: '03.'
-    folder: programmes
-    template: modular
-    lang: ''
-    header:
-      title: Gouvernance
-      content:
-        items: '@self.modular'
-    root: false
-    markdown: ''
-    slug: programmes
-    name: modular.md

+ 0 - 31
tmp/forms/m8co5qkkf12uur4tra88dbie5t/e75346bd00d0b9954ac33c8fde35d73f/index.yaml

@@ -1,31 +0,0 @@
-form: flex-pages
-unique_id: e75346bd00d0b9954ac33c8fde35d73f
-url: '/pages/gouvernance/conseil-dadministration/_president/:add'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1614874025
-  updated: 1614874025
-data: null
-files: {  }
-object:
-  type: pages
-  key: gouvernance/conseil-dadministration/_president
-  storage_key: ''
-  timestamp: 0
-  serialized:
-    __META: {  }
-    storage_key: ''
-    parent_key: 03.gouvernance/01.conseil-dadministration
-    order: false
-    folder: _president
-    template: features
-    lang: ''
-    root: false
-    route: /gouvernance/conseil-dadministration
-    name: modular/features
-    modular: '1'
-    header:
-      title: president
-      body_classes: modular

+ 0 - 84
tmp/forms/oocdaafg1m81alcgel4g63t6cs/a44b13d8025f9fb13b03184c68bd181f/index.yaml

@@ -1,84 +0,0 @@
-form: flex-pages-raw
-unique_id: a44b13d8025f9fb13b03184c68bd181f
-url: 'http://localhost:8000/admin/pages/id%C3%A9e%20page%20simple'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1615307485
-  updated: 1615307507
-data:
-  header:
-    title: null
-    media_order: null
-    body_classes: null
-    visible: false
-    order_by: null
-    order_manual: null
-    published: null
-    date: null
-    publish_date: null
-    unpublish_date: null
-    metadata: null
-    taxonomy: null
-    dateformat: null
-    menu: null
-    slug: null
-    redirect: null
-    process: null
-    twig_first: null
-    never_cache_twig: null
-    child_type: null
-    routable: null
-    cache_enable: null
-    debugger: null
-    template: null
-    append_url_extension: null
-    routes:
-      default: null
-      canonical: null
-      aliases: null
-    admin:
-      children_display_order: null
-    login:
-      visibility_requires_access: null
-    access: null
-    permissions:
-      inherit: null
-      authors: null
-      groups: null
-  content: null
-  folder: 'idée page simple'
-  route: null
-  name: default
-  ordering: true
-  order: null
-  blueprint: null
-  lang: null
-  xss_check: null
-  value: null
-  _post_entries_save: null
-files: {  }
-object:
-  type: pages
-  key: 'idée page simple'
-  storage_key: '02.idée page simple'
-  timestamp: 1615307353
-  serialized:
-    __META:
-      key: 'idée page simple'
-      storage_key: '02.idée page simple'
-      template: null
-      storage_timestamp: 1615307353
-      checksum: 8fd86ee78271bf8fc34c0683a71a03f6
-    storage_key: '02.idée page simple'
-    parent_key: ''
-    order: '02.'
-    folder: 'idée page simple'
-    template: default
-    lang: ''
-    header: {  }
-    root: false
-    markdown: ''
-    slug: 'idée page simple'
-    name: default.md

+ 0 - 29
tmp/forms/oocdaafg1m81alcgel4g63t6cs/e2b1fd25908ccd5d1621269a6a54fa1b/index.yaml

@@ -1,29 +0,0 @@
-form: flex-pages
-unique_id: e2b1fd25908ccd5d1621269a6a54fa1b
-url: '/pages/gouvernanca/:add'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1615305090
-  updated: 1615305090
-data: null
-files: {  }
-object:
-  type: pages
-  key: gouvernanca
-  storage_key: ''
-  timestamp: 0
-  serialized:
-    __META: {  }
-    storage_key: ''
-    parent_key: ''
-    order: '02.'
-    folder: gouvernanca
-    template: default
-    lang: ''
-    root: false
-    route: /
-    name: default
-    header:
-      title: Gouvernanca

+ 0 - 31
tmp/forms/oocdaafg1m81alcgel4g63t6cs/f8b847bb606447afa42e342e90a112bf/index.yaml

@@ -1,31 +0,0 @@
-form: flex-pages
-unique_id: f8b847bb606447afa42e342e90a112bf
-url: '/pages/programmes/_european/:add'
-user:
-  username: admin
-  email: ouidade@figureslibres.io
-timestamps:
-  created: 1615304429
-  updated: 1615304429
-data: null
-files: {  }
-object:
-  type: pages
-  key: programmes/_european
-  storage_key: ''
-  timestamp: 0
-  serialized:
-    __META: {  }
-    storage_key: ''
-    parent_key: 02.programmes
-    order: false
-    folder: _european
-    template: features
-    lang: ''
-    root: false
-    route: /programmes
-    name: modular/features
-    modular: '1'
-    header:
-      title: European
-      body_classes: modular

+ 0 - 3
user/blueprints/pages/modular/programmes.yaml

@@ -43,6 +43,3 @@ form:
                 .logo:
                   type: file
                   label: Logo
-                  accept:
-                    - image/*
-                    - .svg

+ 3 - 3
user/config/site.yaml

@@ -1,8 +1,8 @@
 title: 'GIP EPAU'
-default_lang: en
+default_lang: fr
 author:
-  name: 'Joe Bloggs'
-  email: joe@example.com
+  name: 'Ouidade Soussi'
+  email: ouidade@figureslibres.io
 taxonomies:
   - category
   - tag

+ 8 - 9
user/config/system.yaml

@@ -1,6 +1,5 @@
 absolute_urls: false
 timezone: null
-default_locale: null
 param_sep: ':'
 wrapped_site: false
 reverse_proxy_setup: false
@@ -25,7 +24,6 @@ languages:
   session_store_active: false
   http_accept_language: false
   override_locale: false
-  content_fallback: {  }
   pages_fallback_only: false
 home:
   alias: /home
@@ -52,10 +50,10 @@ pages:
     page: true
     twig: true
   markdown:
-    extra: false
-    auto_line_breaks: false
-    auto_url_links: false
-    escape_markup: false
+    extra: true
+    auto_line_breaks: true
+    auto_url_links: true
+    escape_markup: true
     special_chars:
       '>': gt
       '<': lt
@@ -79,9 +77,9 @@ pages:
   last_modified: false
   etag: true
   vary_accept_encoding: false
-  redirect_default_route: false
   redirect_default_code: '302'
-  redirect_trailing_slash: true
+  redirect_trailing_slash: 1
+  redirect_default_route: 0
   ignore_files:
     - .DS_Store
   ignore_folders:
@@ -141,6 +139,7 @@ assets:
   js_pipeline_before_excludes: true
   js_minify: true
   enable_asset_timestamp: false
+  enable_asset_sri: false
   collections:
     jquery: 'system://assets/jquery/jquery-2.x.min.js'
 errors:
@@ -163,7 +162,7 @@ images:
   cache_perms: '0755'
   debug: false
   auto_fix_orientation: true
-  seofriendly: false
+  seofriendly: true
   defaults:
     loading: auto
 media:

+ 0 - 0
user/pages/01.home/02._programmes/Europan France.jpg → user/pages/01.home/02._programmes/europan_france.jpg


+ 0 - 0
user/pages/01.home/02._programmes/POPSU2018_LOGO_moinsde6cm_sscartouche.png → user/pages/01.home/02._programmes/popsu.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
user/pages/01.home/02._programmes/programmes.md


+ 1 - 2
user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md

@@ -1,11 +1,10 @@
 ---
-title: 'Équipe EPAU title'
+title: 'Équipe EPAU'
 content:
     items: '@self.modular'
 visible: true
 debugger: true
 template: modular/personnes
-
 body_classes: 'modular-row personnes'
 personnes:
     -

+ 12 - 1
user/themes/epau-antimatter/templates/modular/programmes.html.twig

@@ -1,5 +1,6 @@
 <div class="modular-row programmes{{ page.header.class}}">
     {{ content|raw }}
+     {{ dump(page.header) }} 
     <div class="feature-items">
     {% for programme in page.header.programmes %}
            <div class="feature">
@@ -16,10 +17,20 @@
             <p>{{ programme.texte_de_presentation }}</p>
             {% endif %}
             {% if programme.logo %}
-            <img src="{{ page.media[page.header.programmes.logo] }}" alt=“logo_programme” />
+            <img src="{{ page.media[page.header.programmes.logo].html() }}" alt=logo_programme />
+
+
             {% endif %}
             </div>
         </div>
     {% endfor %}
     </div>
 </div>
+
+
+
+{# ça, ça marche mais maintenant faut créer un variable qui va chercher les images
+{% if programme.logo %}
+<img src="{{ page.media['popsu.png'].url|e }}" alt=“logo_programme” />
+{% endif %}
+#}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است