Ver código fonte

Merge branch 'master' of gogs-figureslibres.io:ouidade/dev-epau.archi.fr

bach 4 anos atrás
pai
commit
964832e90b
100 arquivos alterados com 1451 adições e 1354 exclusões
  1. 0 66
      .github/workflows/build.yaml
  2. 0 72
      .github/workflows/tests.yaml
  3. 50 1
      CHANGELOG.md
  4. 0 23
      README.md
  5. 8 2
      SECURITY.md
  6. 5 3
      composer.json
  7. 193 180
      composer.lock
  8. 39 20
      system/blueprints/config/system.yaml
  9. 1 1
      system/blueprints/flex/pages.yaml
  10. 8 0
      system/blueprints/pages/default.yaml
  11. 4 3
      system/config/system.yaml
  12. 1 1
      system/defines.php
  13. 26 0
      system/src/Grav/Common/Assets/BaseAsset.php
  14. 1 1
      system/src/Grav/Common/Assets/Css.php
  15. 1 1
      system/src/Grav/Common/Assets/Js.php
  16. 3 2
      system/src/Grav/Common/Assets/Pipeline.php
  17. 4 2
      system/src/Grav/Common/Cache.php
  18. 14 9
      system/src/Grav/Common/Data/Validation.php
  19. 42 8
      system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
  20. 65 10
      system/src/Grav/Common/Flex/Types/Pages/PageObject.php
  21. 21 8
      system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
  22. 2 0
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
  23. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
  24. 12 6
      system/src/Grav/Common/Grav.php
  25. 4 4
      system/src/Grav/Common/Helpers/LogViewer.php
  26. 1 1
      system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
  27. 2 2
      system/src/Grav/Common/Page/Markdown/Excerpts.php
  28. 11 0
      system/src/Grav/Common/Page/Medium/AbstractMedia.php
  29. 13 11
      system/src/Grav/Common/Page/Page.php
  30. 144 79
      system/src/Grav/Common/Page/Pages.php
  31. 21 13
      system/src/Grav/Common/Page/Types.php
  32. 53 34
      system/src/Grav/Common/Plugin.php
  33. 27 11
      system/src/Grav/Common/Plugins.php
  34. 5 4
      system/src/Grav/Common/Processors/InitializeProcessor.php
  35. 36 22
      system/src/Grav/Common/Service/PagesServiceProvider.php
  36. 15 35
      system/src/Grav/Common/Theme.php
  37. 18 3
      system/src/Grav/Common/Themes.php
  38. 7 10
      system/src/Grav/Common/Uri.php
  39. 112 33
      system/src/Grav/Common/Utils.php
  40. 3 2
      system/src/Grav/Console/Cli/CleanCommand.php
  41. 51 0
      system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
  42. 1 1
      system/src/Grav/Framework/Flex/FlexIndex.php
  43. 8 0
      system/src/Grav/Framework/Flex/FlexObject.php
  44. 40 18
      system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
  45. 12 6
      system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
  46. 8 14
      system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
  47. 24 18
      system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php
  48. 2 2
      system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php
  49. 2 0
      system/src/Grav/Framework/Flex/Storage/FileStorage.php
  50. 38 33
      system/src/Grav/Framework/Flex/Storage/FolderStorage.php
  51. 9 1
      system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
  52. 69 13
      system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
  53. 0 87
      tmp/forms/fg86637jgl2sbvcbloavimfam4/73f35c84f67d9518064f6783910db89a/index.yaml
  54. 0 29
      tmp/forms/fg86637jgl2sbvcbloavimfam4/ae2c7e0ffb4b4b1c4654412e6429a17c/index.yaml
  55. 0 50
      tmp/forms/fg86637jgl2sbvcbloavimfam4/b24e06058e6bf2c3e455378b84824a00/index.yaml
  56. 0 53
      tmp/forms/m8co5qkkf12uur4tra88dbie5t/cdb3ae30b3dab66b186228ae772dd1a0/index.yaml
  57. 0 31
      tmp/forms/m8co5qkkf12uur4tra88dbie5t/e75346bd00d0b9954ac33c8fde35d73f/index.yaml
  58. 0 84
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/a44b13d8025f9fb13b03184c68bd181f/index.yaml
  59. 0 29
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/e2b1fd25908ccd5d1621269a6a54fa1b/index.yaml
  60. 0 31
      tmp/forms/oocdaafg1m81alcgel4g63t6cs/f8b847bb606447afa42e342e90a112bf/index.yaml
  61. 1 1
      user/blueprints/pages/modular/personnes.yaml
  62. 14 11
      user/blueprints/pages/modular/programmes.yaml
  63. 48 0
      user/blueprints/pages/modular/rapport_dactivitees.yaml
  64. 3 3
      user/config/site.yaml
  65. 11 12
      user/config/system.yaml
  66. 2 2
      user/pages/01.home/01._accueil/text.md
  67. BIN
      user/pages/01.home/02._programmes/10361_web_01.jpg
  68. BIN
      user/pages/01.home/02._programmes/14360_default_big.jpg
  69. 0 0
      user/pages/01.home/02._programmes/europan_france.jpg
  70. 0 7
      user/pages/01.home/02._programmes/features.md
  71. BIN
      user/pages/01.home/02._programmes/hyperliens_marseille.PNG
  72. BIN
      user/pages/01.home/02._programmes/logo_coubertin.png
  73. 0 0
      user/pages/01.home/02._programmes/popsu.png
  74. 6 0
      user/pages/01.home/02._programmes/programmes.md
  75. BIN
      user/pages/01.home/03._rapports-dactivitees/rapport_activite_V1.pdf
  76. BIN
      user/pages/01.home/03._rapports-dactivitees/rapport_activite_V1.png
  77. 39 0
      user/pages/01.home/03._rapports-dactivitees/rapport_dactivitees.md
  78. BIN
      user/pages/01.home/03._rapports-dactivites/images.jpeg
  79. 0 13
      user/pages/01.home/03._rapports-dactivites/showcase.md
  80. 19 0
      user/pages/01.home/04._gouvernance/01._conseil-dadministration/01._presidence/personnes.md
  81. 16 0
      user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md
  82. 0 0
      user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/photo_butlen.jpeg
  83. 20 0
      user/pages/01.home/04._gouvernance/01._conseil-dadministration/conseil_dadministration.md
  84. BIN
      user/pages/01.home/04._gouvernance/02._equipe-epau/Alain Maugard_TROUVE.jpg
  85. 6 17
      user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md
  86. 20 0
      user/pages/01.home/04._gouvernance/gouvernance.md
  87. 1 0
      user/pages/01.home/EPAU_logo_EPAU_vecto-seul.svg
  88. BIN
      user/pages/01.home/Logotype_GIP_EPAU.svg.png
  89. 6 0
      user/pages/01.home/Republique-francaise-logo.svg
  90. 2 2
      user/pages/01.home/modular.md
  91. 0 0
      user/pages/01.home/popsu.png
  92. 0 19
      user/pages/02.programmes/01.popsu/default.md
  93. BIN
      user/pages/02.programmes/02.european/Europan France.jpg
  94. 0 18
      user/pages/02.programmes/02.european/default.md
  95. 0 16
      user/pages/02.programmes/03.coubertin/item.md
  96. BIN
      user/pages/02.programmes/04.forum-des-solutions/forum_solution.png
  97. 0 20
      user/pages/02.programmes/04.forum-des-solutions/item.md
  98. 0 5
      user/pages/02.programmes/05.rapports-dactivites/default.md
  99. 0 17
      user/pages/02.programmes/modular.md
  100. 0 7
      user/pages/03.gouvernance/01.conseil-dadministration/item.md

+ 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'

+ 50 - 1
CHANGELOG.md

@@ -1,8 +1,57 @@
+# v1.7.9
+## 03/19/2021
+
+1. [](#new)
+    * Added `Media::hide()` method to hide files from media
+    * Added `Utils::getPathFromToken()` method which works also with `Flex Objects`
+    * Added `FlexMediaTrait::getMediaField()`, which can be used to access custom media set in the blueprint fields
+    * Added `FlexMediaTrait::getFieldSettings()`, which can be used to get media field settings
+1. [](#improved)
+    * Method `Utils::getPagePathFromToken()` now calls the more generic `Utils::getPathFromToken()`
+    * Updated `SECURITY.md` to use security@getgrav.org
+1. [](#bugfix)
+    * Fixed broken media upload in `Flex` with `@self/path`, `@page` and `@theme` destinations [#3275](https://github.com/getgrav/grav/issues/3275)
+    * Fixed media fields excluding newly deleted files before saving the object
+    * Fixed method `$pages->find()` should never redirect [#3266](https://github.com/getgrav/grav/pull/3266)
+    * Fixed `Page::activeChild()` throwing an error [#3276](https://github.com/getgrav/grav/issues/3276)
+    * Fixed `Flex Page` CRUD ACL when creating a new page (needs Flex Objects plugin update) [grav-plugin-flex-objects#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115)
+    * Fixed the list of pages not showing up in admin [#3280](https://github.com/getgrav/grav/issues/3280)
+    * Fixed text field min/max validation for UTF8 characters [#3281](https://github.com/getgrav/grav/issues/3281)
+    * Fixed redirects using wrong redirect code
+
+# v1.7.8
+## 03/17/2021
+
+1. [](#new)
+    * Added `ControllerResponseTrait::createDownloadResponse()` method
+    * Added full blueprint support to theme if you move existing files in `blueprints/` to `blueprints/pages/` folder [#3255](https://github.com/getgrav/grav/issues/3255)
+    * Added support for `Theme::getFormFieldTypes()` just like in plugins
+1. [](#improved)
+    * Optimized `Flex Pages` for speed
+    * Optimized saving visible/ordered pages when there are a lot of siblings [#3231](https://github.com/getgrav/grav/issues/3231)
+    * Clearing cache now deletes all clockwork files
+    * Improved `system.pages.redirect_default_route` and `system.pages.redirect_trailing_slash` configuration options to accept redirect code
+1. [](#bugfix)
+    * Fixed clockwork error when clearing cache
+    * Fixed missing method `translated()` in `Flex Pages`
+    * Fixed missing `Flex Pages` in site if multi-language support has been enabled
+    * Fixed Grav using blueprints and form fields from disabled plugins
+    * Fixed `FlexIndex::sortBy(['key' => 'ASC'])` having no effect
+    * Fixed default Flex Pages collection ordering to order by filesystem path
+    * Fixed disappearing pages on save if `pages://` stream resolves to multiple folders where the preferred folder doesn't exist
+    * Fixed Markdown image attribute `loading` [#3251](https://github.com/getgrav/grav/pull/3251)
+    * Fixed `Uri::isValidExtension()` returning false positives
+    * Fixed `page.html` returning duplicated content with `system.pages.redirect_default_route` turned on [#3130](https://github.com/getgrav/grav/issues/3130)
+    * Fixed site redirect with redirect code failing when redirecting to sub-pages [#3035](https://github.com/getgrav/grav/pull/3035/files)
+    * Fixed `Uncaught ValueError: Path cannot be empty` when failing to upload a file [#3265](https://github.com/getgrav/grav/issues/3265)
+    * Fixed `Path cannot be empty` when viewing non-existent log file [#3270](https://github.com/getgrav/grav/issues/3270)
+    * Fixed `onAdminSave` original page having empty header [#3259](https://github.com/getgrav/grav/issues/3259)
+
 # v1.7.7
 ## 02/23/2021
 
 1. [](#new)
-   * Added `Utils::arrayToQueryParams()` to convert an array into query params
+    * Added `Utils::arrayToQueryParams()` to convert an array into query params
 1. [](#improved)
     * Added original image support for all flex objects and media fields
     * Improved `Pagination` class to allow custom pagination query parameter

+ 0 - 23
README.md

@@ -148,26 +148,3 @@ 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
-
-
-
-
-# EPAU REad ME
-
-
-- intall grav avec composer
-
-composer create-project getgrav/grav ~/webroot/grav
-- lance serveur local
-cd ~/webroot/nomdusite
-php -S localhost:8000 system/router.php
-Fenêtre à laisser ouverte!
-
-
-- dans autre fenetre, installer version admin
-cd ~/webroot/nomdusite
-bin/gpm selfupgrade  (mise à jours)
-
-bin/gpm install admin   (installe l’admin)
-
-bin/plugin login newuser   (créer un nouvel utilisateur)

+ 8 - 2
SECURITY.md

@@ -7,9 +7,15 @@ We are focusing our security updates on the following versions
 | Version | Supported          |
 | ------- | ------------------ |
 | 1.7.x   | :white_check_mark: |
-| 1.6.x   | :white_check_mark: |
+| 1.6.x   | :warning:          |
 | < 1.6   | :x:                |
 
+## :warning: Versions
+
+Versions with :warning: will be supported for security issues, however you won't be able to update to them, you will need to manually update through the [`direct-install` command](https://learn.getgrav.org/17/admin-panel/tools).
+
+If you cannot update to the latest stable version available because, for example, your server does not meet the minimum PHP requirements, you can manually install a previous version by downloading the package from our Releases directory (https://github.com/getgrav/grav/releases).
+
 ## Reporting a Vulnerability
 
-Please contact contact@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible.
+Please contact security@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible.

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

+ 193 - 180
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "32b6cbbe234714397aea3c6ed1eddf6b",
+    "content-hash": "4ae6fc7274c018b1bb34bb1b80bd62c5",
     "packages": [
         {
             "name": "antoligy/dom-string-iterators",
@@ -381,16 +381,16 @@
         },
         {
             "name": "donatj/phpuseragentparser",
-            "version": "v1.2.0",
+            "version": "v1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/donatj/PhpUserAgent.git",
-                "reference": "978e66786bc392a09b24b152a8a695dadd230e60"
+                "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/978e66786bc392a09b24b152a8a695dadd230e60",
-                "reference": "978e66786bc392a09b24b152a8a695dadd230e60",
+                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/246c1cf0a44f07168c702203bf30d5f48f17bab0",
+                "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0",
                 "shasum": ""
             },
             "require": {
@@ -433,11 +433,11 @@
             ],
             "support": {
                 "issues": "https://github.com/donatj/PhpUserAgent/issues",
-                "source": "https://github.com/donatj/PhpUserAgent/tree/v1.2.0"
+                "source": "https://github.com/donatj/PhpUserAgent/tree/v1.4.0"
             },
             "funding": [
                 {
-                    "url": "https://www.paypal.me/donatj/15",
+                    "url": "https://www.paypal.me/donatj/5",
                     "type": "custom"
                 },
                 {
@@ -445,7 +445,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-12-29T05:36:08+00:00"
+            "time": "2021-03-16T16:25:14+00:00"
         },
         {
             "name": "dragonmantank/cron-expression",
@@ -642,16 +642,16 @@
         },
         {
             "name": "filp/whoops",
-            "version": "2.9.2",
+            "version": "2.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "df7933820090489623ce0be5e85c7e693638e536"
+                "reference": "6ecda5217bf048088b891f7403b262906be5a957"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536",
-                "reference": "df7933820090489623ce0be5e85c7e693638e536",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/6ecda5217bf048088b891f7403b262906be5a957",
+                "reference": "6ecda5217bf048088b891f7403b262906be5a957",
                 "shasum": ""
             },
             "require": {
@@ -701,7 +701,7 @@
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.9.2"
+                "source": "https://github.com/filp/whoops/tree/2.10.0"
             },
             "funding": [
                 {
@@ -709,7 +709,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-01-24T12:00:00+00:00"
+            "time": "2021-03-16T12:00:00+00:00"
         },
         {
             "name": "gregwar/cache",
@@ -763,18 +763,18 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/getgrav/Image.git",
-                "reference": "70afaa75ea19856813124142c51f5fb2e9f1a285"
+                "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/getgrav/Image/zipball/70afaa75ea19856813124142c51f5fb2e9f1a285",
-                "reference": "70afaa75ea19856813124142c51f5fb2e9f1a285",
+                "url": "https://api.github.com/repos/getgrav/Image/zipball/ea23859700f32447a85e79d96f331e3d6c8897a8",
+                "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8",
                 "shasum": ""
             },
             "require": {
                 "ext-gd": "*",
                 "gregwar/cache": "dev-php8",
-                "php": "^5.3 || ^7.0 || ^8.0"
+                "php": "^5.6 || ^7.0 || ^8.0"
             },
             "require-dev": {
                 "sllh/php-cs-fixer-styleci-bridge": "~1.0",
@@ -808,7 +808,7 @@
             "support": {
                 "source": "https://github.com/getgrav/Image/tree/php8"
             },
-            "time": "2020-12-02T14:04:28+00:00"
+            "time": "2021-03-15T17:03:52+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -887,16 +887,16 @@
         },
         {
             "name": "itsgoingd/clockwork",
-            "version": "v5.0.6",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/itsgoingd/clockwork.git",
-                "reference": "1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1"
+                "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1",
-                "reference": "1de3f9f9fc22217aa024f79ecbdf0fde418fc0a1",
+                "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4",
+                "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4",
                 "shasum": ""
             },
             "require": {
@@ -944,7 +944,7 @@
             ],
             "support": {
                 "issues": "https://github.com/itsgoingd/clockwork/issues",
-                "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.6"
+                "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.7"
             },
             "funding": [
                 {
@@ -952,65 +952,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-12-27T00:18:25+00:00"
-        },
-        {
-            "name": "kodus/psr7-server",
-            "version": "1.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/kodus/psr7-server.git",
-                "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/kodus/psr7-server/zipball/dcfd0116451b0f0e7c6b23b831757ed288347278",
-                "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1",
-                "psr/http-factory": "^1.0",
-                "psr/http-message": "^1.0"
-            },
-            "replace": {
-                "nyholm/psr7-server": "^0.3"
-            },
-            "require-dev": {
-                "nyholm/nsa": "^1.1",
-                "nyholm/psr7": "^1.0",
-                "phpunit/phpunit": "^7.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Nyholm\\Psr7Server\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Tobias Nyholm",
-                    "email": "tobias.nyholm@gmail.com"
-                },
-                {
-                    "name": "Martijn van der Ven",
-                    "email": "martijn@vanderven.se"
-                }
-            ],
-            "description": "Helper classes to handle PSR-7 server requests",
-            "homepage": "http://tnyholm.se",
-            "keywords": [
-                "psr-17",
-                "psr-7"
-            ],
-            "support": {
-                "source": "https://github.com/kodus/psr7-server/tree/master"
-            },
-            "time": "2019-06-17T10:48:13+00:00"
+            "time": "2021-03-14T16:29:40+00:00"
         },
         {
             "name": "league/climate",
@@ -1421,16 +1363,16 @@
         },
         {
             "name": "nyholm/psr7",
-            "version": "1.3.2",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Nyholm/psr7.git",
-                "reference": "a272953743c454ac4af9626634daaf5ab3ce1173"
+                "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a272953743c454ac4af9626634daaf5ab3ce1173",
-                "reference": "a272953743c454ac4af9626634daaf5ab3ce1173",
+                "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b",
+                "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b",
                 "shasum": ""
             },
             "require": {
@@ -1452,7 +1394,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "1.4-dev"
                 }
             },
             "autoload": {
@@ -1482,7 +1424,73 @@
             ],
             "support": {
                 "issues": "https://github.com/Nyholm/psr7/issues",
-                "source": "https://github.com/Nyholm/psr7/tree/1.3.2"
+                "source": "https://github.com/Nyholm/psr7/tree/1.4.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Zegnat",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nyholm",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-02-18T15:41:32+00:00"
+        },
+        {
+            "name": "nyholm/psr7-server",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Nyholm/psr7-server.git",
+                "reference": "5c134aeb5dd6521c7978798663470dabf0528c96"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/5c134aeb5dd6521c7978798663470dabf0528c96",
+                "reference": "5c134aeb5dd6521c7978798663470dabf0528c96",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0",
+                "psr/http-factory": "^1.0",
+                "psr/http-message": "^1.0"
+            },
+            "require-dev": {
+                "nyholm/nsa": "^1.1",
+                "nyholm/psr7": "^1.3",
+                "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Nyholm\\Psr7Server\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com"
+                },
+                {
+                    "name": "Martijn van der Ven",
+                    "email": "martijn@vanderven.se"
+                }
+            ],
+            "description": "Helper classes to handle PSR-7 server requests",
+            "homepage": "http://tnyholm.se",
+            "keywords": [
+                "psr-17",
+                "psr-7"
+            ],
+            "support": {
+                "issues": "https://github.com/Nyholm/psr7-server/issues",
+                "source": "https://github.com/Nyholm/psr7-server/tree/1.0.1"
             },
             "funding": [
                 {
@@ -1494,7 +1502,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-11-14T17:35:34+00:00"
+            "time": "2020-11-15T15:26:20+00:00"
         },
         {
             "name": "phive/twig-extensions-deferred",
@@ -2238,16 +2246,16 @@
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "24026c44fc37099fa145707fecd43672831b837a"
+                "reference": "c98349bda966c70d6c08b4cd8658377c94166492"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/24026c44fc37099fa145707fecd43672831b837a",
-                "reference": "24026c44fc37099fa145707fecd43672831b837a",
+                "url": "https://api.github.com/repos/symfony/console/zipball/c98349bda966c70d6c08b4cd8658377c94166492",
+                "reference": "c98349bda966c70d6c08b4cd8658377c94166492",
                 "shasum": ""
             },
             "require": {
@@ -2307,7 +2315,7 @@
             "description": "Eases the creation of beautiful and testable command line interfaces",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/console/tree/v4.4.19"
+                "source": "https://github.com/symfony/console/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -2323,7 +2331,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-27T09:09:26+00:00"
+            "time": "2021-02-22T18:44:15+00:00"
         },
         {
             "name": "symfony/contracts",
@@ -2421,7 +2429,7 @@
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
@@ -2484,7 +2492,7 @@
             "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.19"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -2504,16 +2512,16 @@
         },
         {
             "name": "symfony/http-client",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-client.git",
-                "reference": "d8df50fe9229576b254c6822eb5cfff36c02c967"
+                "reference": "67c5af7489b3c2eea771abd973243f5c58f5fb40"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-client/zipball/d8df50fe9229576b254c6822eb5cfff36c02c967",
-                "reference": "d8df50fe9229576b254c6822eb5cfff36c02c967",
+                "url": "https://api.github.com/repos/symfony/http-client/zipball/67c5af7489b3c2eea771abd973243f5c58f5fb40",
+                "reference": "67c5af7489b3c2eea771abd973243f5c58f5fb40",
                 "shasum": ""
             },
             "require": {
@@ -2527,7 +2535,7 @@
                 "php-http/async-client-implementation": "*",
                 "php-http/client-implementation": "*",
                 "psr/http-client-implementation": "1.0",
-                "symfony/http-client-implementation": "1.1"
+                "symfony/http-client-implementation": "1.1|2.0"
             },
             "require-dev": {
                 "guzzlehttp/promises": "^1.4",
@@ -2564,7 +2572,7 @@
             "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-client/tree/v4.4.19"
+                "source": "https://github.com/symfony/http-client/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -2580,7 +2588,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-27T09:09:26+00:00"
+            "time": "2021-02-25T18:06:45+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
@@ -2986,7 +2994,7 @@
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
@@ -3027,7 +3035,7 @@
             "description": "Executes commands in sub-processes",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/process/tree/v4.4.19"
+                "source": "https://github.com/symfony/process/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -3047,7 +3055,7 @@
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
@@ -3116,7 +3124,7 @@
                 "dump"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-dumper/tree/v4.4.19"
+                "source": "https://github.com/symfony/var-dumper/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -3136,16 +3144,16 @@
         },
         {
             "name": "symfony/yaml",
-            "version": "v4.4.19",
+            "version": "v4.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9"
+                "reference": "29e61305e1c79d25f71060903982ead8f533e267"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9",
-                "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/29e61305e1c79d25f71060903982ead8f533e267",
+                "reference": "29e61305e1c79d25f71060903982ead8f533e267",
                 "shasum": ""
             },
             "require": {
@@ -3187,7 +3195,7 @@
             "description": "Loads and dumps YAML files",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/yaml/tree/v4.4.19"
+                "source": "https://github.com/symfony/yaml/tree/v4.4.20"
             },
             "funding": [
                 {
@@ -3203,7 +3211,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-27T09:09:26+00:00"
+            "time": "2021-02-22T15:36:50+00:00"
         },
         {
             "name": "twig/twig",
@@ -3407,16 +3415,16 @@
         },
         {
             "name": "codeception/codeception",
-            "version": "4.1.17",
+            "version": "4.1.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Codeception/Codeception.git",
-                "reference": "c153b1ab289b3e3109e685379aa8847c54ac2b68"
+                "reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Codeception/Codeception/zipball/c153b1ab289b3e3109e685379aa8847c54ac2b68",
-                "reference": "c153b1ab289b3e3109e685379aa8847c54ac2b68",
+                "url": "https://api.github.com/repos/Codeception/Codeception/zipball/f47547bac347dfb5ea5351ff91148cbcc08e6818",
+                "reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818",
                 "shasum": ""
             },
             "require": {
@@ -3490,7 +3498,7 @@
             ],
             "support": {
                 "issues": "https://github.com/Codeception/Codeception/issues",
-                "source": "https://github.com/Codeception/Codeception/tree/4.1.17"
+                "source": "https://github.com/Codeception/Codeception/tree/4.1.18"
             },
             "funding": [
                 {
@@ -3498,7 +3506,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2021-02-01T07:30:47+00:00"
+            "time": "2021-02-23T17:11:42+00:00"
         },
         {
             "name": "codeception/lib-asserts",
@@ -3556,16 +3564,16 @@
         },
         {
             "name": "codeception/lib-innerbrowser",
-            "version": "1.4.0",
+            "version": "1.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Codeception/lib-innerbrowser.git",
-                "reference": "b7406c710684c255d9b067d7795269a5585a0406"
+                "reference": "693e116f81ef98eae98c43ef785a726faf87394e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/b7406c710684c255d9b067d7795269a5585a0406",
-                "reference": "b7406c710684c255d9b067d7795269a5585a0406",
+                "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/693e116f81ef98eae98c43ef785a726faf87394e",
+                "reference": "693e116f81ef98eae98c43ef785a726faf87394e",
                 "shasum": ""
             },
             "require": {
@@ -3610,9 +3618,9 @@
             ],
             "support": {
                 "issues": "https://github.com/Codeception/lib-innerbrowser/issues",
-                "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.0"
+                "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.1"
             },
-            "time": "2021-01-29T18:17:25+00:00"
+            "time": "2021-03-02T08:01:54+00:00"
         },
         {
             "name": "codeception/module-asserts",
@@ -3987,16 +3995,16 @@
         },
         {
             "name": "guzzlehttp/promises",
-            "version": "1.4.0",
+            "version": "1.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "60d379c243457e073cff02bc323a2a86cb355631"
+                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
-                "reference": "60d379c243457e073cff02bc323a2a86cb355631",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
+                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
                 "shasum": ""
             },
             "require": {
@@ -4036,9 +4044,9 @@
             ],
             "support": {
                 "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/1.4.0"
+                "source": "https://github.com/guzzle/promises/tree/1.4.1"
             },
-            "time": "2020-09-30T07:37:28+00:00"
+            "time": "2021-03-07T09:25:29+00:00"
         },
         {
             "name": "myclabs/deep-copy",
@@ -4216,16 +4224,16 @@
         },
         {
             "name": "phar-io/version",
-            "version": "3.0.4",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/version.git",
-                "reference": "e4782611070e50613683d2b9a57730e9a3ba5451"
+                "reference": "bae7c545bef187884426f042434e561ab1ddb182"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451",
-                "reference": "e4782611070e50613683d2b9a57730e9a3ba5451",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+                "reference": "bae7c545bef187884426f042434e561ab1ddb182",
                 "shasum": ""
             },
             "require": {
@@ -4261,9 +4269,9 @@
             "description": "Library for handling version information and constraints",
             "support": {
                 "issues": "https://github.com/phar-io/version/issues",
-                "source": "https://github.com/phar-io/version/tree/3.0.4"
+                "source": "https://github.com/phar-io/version/tree/3.1.0"
             },
-            "time": "2020-12-13T23:18:30+00:00"
+            "time": "2021-02-23T14:00:09+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
@@ -4492,16 +4500,16 @@
         },
         {
             "name": "phpstan/phpstan",
-            "version": "0.12.77",
+            "version": "0.12.81",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "1f10b8c8d118d01e7b492f9707999d456be5812c"
+                "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1f10b8c8d118d01e7b492f9707999d456be5812c",
-                "reference": "1f10b8c8d118d01e7b492f9707999d456be5812c",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0dd5b0ebeff568f7000022ea5f04aa86ad3124b8",
+                "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8",
                 "shasum": ""
             },
             "require": {
@@ -4532,7 +4540,7 @@
             "description": "PHPStan - PHP Static Analysis Tool",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/0.12.77"
+                "source": "https://github.com/phpstan/phpstan/tree/0.12.81"
             },
             "funding": [
                 {
@@ -4548,7 +4556,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-02-17T16:22:19+00:00"
+            "time": "2021-03-08T22:03:02+00:00"
         },
         {
             "name": "phpstan/phpstan-deprecation-rules",
@@ -4921,16 +4929,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.5.2",
+            "version": "9.5.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4"
+                "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f661659747f2f87f9e72095bb207bceb0f151cb4",
-                "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/27241ac75fc37ecf862b6e002bf713b6566cbe41",
+                "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41",
                 "shasum": ""
             },
             "require": {
@@ -5008,7 +5016,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.2"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.3"
             },
             "funding": [
                 {
@@ -5020,7 +5028,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-02-02T14:45:58+00:00"
+            "time": "2021-03-17T07:30:34+00:00"
         },
         {
             "name": "psr/http-client",
@@ -6040,16 +6048,16 @@
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v5.2.3",
+            "version": "v5.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd"
+                "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/b03b2057ed53ee4eab2e8f372084d7722b7b8ffd",
-                "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3ca3a57ce9860318b20a924fec5daf5c6db44d93",
+                "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93",
                 "shasum": ""
             },
             "require": {
@@ -6091,7 +6099,7 @@
             "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/browser-kit/tree/v5.2.3"
+                "source": "https://github.com/symfony/browser-kit/tree/v5.2.4"
             },
             "funding": [
                 {
@@ -6107,11 +6115,11 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-27T12:56:27+00:00"
+            "time": "2021-02-22T06:48:33+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v5.2.3",
+            "version": "v5.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
@@ -6156,7 +6164,7 @@
             "description": "Converts CSS selectors to XPath expressions",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/css-selector/tree/v5.2.3"
+                "source": "https://github.com/symfony/css-selector/tree/v5.2.4"
             },
             "funding": [
                 {
@@ -6176,16 +6184,16 @@
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v5.2.3",
+            "version": "v5.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384"
+                "reference": "400e265163f65aceee7e904ef532e15228de674b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/5d89ceb53ec65e1973a555072fac8ed5ecad3384",
-                "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b",
+                "reference": "400e265163f65aceee7e904ef532e15228de674b",
                 "shasum": ""
             },
             "require": {
@@ -6230,7 +6238,7 @@
             "description": "Eases DOM navigation for HTML and XML documents",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dom-crawler/tree/v5.2.3"
+                "source": "https://github.com/symfony/dom-crawler/tree/v5.2.4"
             },
             "funding": [
                 {
@@ -6246,20 +6254,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-27T10:01:46+00:00"
+            "time": "2021-02-15T18:55:04+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v5.2.3",
+            "version": "v5.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "4adc8d172d602008c204c2e16956f99257248e03"
+                "reference": "0d639a0943822626290d169965804f79400e6a04"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03",
-                "reference": "4adc8d172d602008c204c2e16956f99257248e03",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04",
+                "reference": "0d639a0943822626290d169965804f79400e6a04",
                 "shasum": ""
             },
             "require": {
@@ -6291,7 +6299,7 @@
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v5.2.3"
+                "source": "https://github.com/symfony/finder/tree/v5.2.4"
             },
             "funding": [
                 {
@@ -6307,7 +6315,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-01-28T22:06:19+00:00"
+            "time": "2021-02-15T18:55:04+00:00"
         },
         {
             "name": "theseer/tokenizer",
@@ -6408,30 +6416,35 @@
         },
         {
             "name": "webmozart/assert",
-            "version": "1.9.1",
+            "version": "1.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozarts/assert.git",
-                "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
+                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
-                "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
+                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0 || ^8.0",
+                "php": "^7.2 || ^8.0",
                 "symfony/polyfill-ctype": "^1.8"
             },
             "conflict": {
                 "phpstan/phpstan": "<0.12.20",
-                "vimeo/psalm": "<3.9.1"
+                "vimeo/psalm": "<4.6.1 || 4.6.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+                "phpunit/phpunit": "^8.5.13"
             },
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.10-dev"
+                }
+            },
             "autoload": {
                 "psr-4": {
                     "Webmozart\\Assert\\": "src/"
@@ -6455,9 +6468,9 @@
             ],
             "support": {
                 "issues": "https://github.com/webmozarts/assert/issues",
-                "source": "https://github.com/webmozarts/assert/tree/1.9.1"
+                "source": "https://github.com/webmozarts/assert/tree/1.10.0"
             },
-            "time": "2020-07-08T17:02:28+00:00"
+            "time": "2021-03-09T10:59:23+00:00"
         }
     ],
     "aliases": [],

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

+ 8 - 0
system/blueprints/pages/default.yaml

@@ -35,10 +35,18 @@ form:
                 validate:
                   type: textarea
 
+
+
             header.media_order:
               type: pagemedia
               label: PLUGIN_ADMIN.PAGE_MEDIA
 
+            background_image:
+              type: filepicker
+              label: image pour fond parallax
+
+
+
         options:
           type: tab
           title: PLUGIN_ADMIN.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.9');
 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;
                             }
                         }

+ 14 - 9
system/src/Grav/Common/Data/Validation.php

@@ -27,7 +27,6 @@ use function is_bool;
 use function is_float;
 use function is_int;
 use function is_string;
-use function strlen;
 
 /**
  * Class Validation
@@ -239,16 +238,20 @@ class Validation
             $value = trim($value);
         }
 
-        if (isset($params['min']) && strlen($value) < $params['min']) {
+        $len = mb_strlen($value);
+
+        $min = (int)($params['min'] ?? 0);
+        if ($min && $len < $min) {
             return false;
         }
 
-        if (isset($params['max']) && strlen($value) > $params['max']) {
+        $max = (int)($params['max'] ?? 0);
+        if ($max && $len > $max) {
             return false;
         }
 
-        $min = $params['min'] ?? 0;
-        if (isset($params['step']) && (strlen($value) - $min) % $params['step'] === 0) {
+        $step = (int)($params['step'] ?? 0);
+        if ($step && ($len - $min) % $step === 0) {
             return false;
         }
 
@@ -271,11 +274,13 @@ class Validation
             return '';
         }
 
+        $value = (string)$value;
+
         if (!empty($params['trim'])) {
             $value = trim($value);
         }
 
-        return (string) $value;
+        return $value;
     }
 
     /**
@@ -332,7 +337,7 @@ class Validation
      */
     protected static function filterLower($value, array $params)
     {
-        return strtolower($value);
+        return mb_strtolower($value);
     }
 
     /**
@@ -342,7 +347,7 @@ class Validation
      */
     protected static function filterUpper($value, array $params)
     {
-        return strtoupper($value);
+        return mb_strtoupper($value);
     }
 
 
@@ -534,7 +539,7 @@ class Validation
      */
     protected static function filterNumber($value, array $params, array $field)
     {
-        return (string)(int)$value !== (string)(float)$value ? (float) $value : (int) $value;
+        return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
     }
 
     /**

+ 42 - 8
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;
@@ -490,12 +522,13 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                     $tmp = $child->children()->getIndex();
                     $child_count = $tmp->count();
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
+                    $route = $child->getRoute();
                     $payload = [
                         'item-key' => basename($child->rawRoute() ?? $child->getKey()),
                         'icon' => $icon,
                         'title' => htmlspecialchars($child->menu()),
                         'route' => [
-                            'display' => $child->getRoute()->toString(false) ?: '/',
+                            'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
                             'raw' => $child->rawRoute(),
                         ],
                         'modified' => $this->jsDate($child->modified()),
@@ -535,20 +568,21 @@ 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';
         }
 

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

@@ -22,6 +22,7 @@ use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait;
 use Grav\Common\Language\Language;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Pages;
+use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\FlexObject;
@@ -62,7 +63,6 @@ class PageObject extends FlexPageObject
 
     /** @var string Language code, eg: 'en' */
     protected $language;
-
     /** @var string File format, eg. 'md' */
     protected $format;
 
@@ -78,6 +78,7 @@ class PageObject extends FlexPageObject
             'path' => true,
             'full_order' => true,
             'filterBy' => true,
+            'translated' => false,
         ] + parent::getCachedMethods();
     }
 
@@ -92,6 +93,11 @@ class PageObject extends FlexPageObject
         }
     }
 
+    public function translated(): bool
+    {
+        return $this->translatedLanguages(true) ? true : false;
+    }
+
     /**
      * @param string|array $query
      * @return Route|null
@@ -223,7 +229,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 +295,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 +317,36 @@ class PageObject extends FlexPageObject
 
         $this->_reorder = [];
         $this->setProperty('parent_key', $parent->getStorageKey());
+        $this->storeOriginal();
 
         return $this;
     }
 
+    /**
+     * @param UserInterface $user
+     * @param string $action
+     * @param string $scope
+     * @param bool $isMe
+     * @return bool|null
+     */
+    protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
+    {
+        // Special case: creating a new page means checking parent for its permissions.
+        if ($action === 'create' && !$this->exists()) {
+            $parent = $this->parent();
+            if ($parent && method_exists($parent, 'isAuthorized')) {
+                return $parent->isAuthorized($action, $scope, $user);
+            }
+
+            return false;
+        }
+
+        return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
+    }
+
     /**
      * @param array $ordering
-     * @return PageCollection
+     * @return PageCollection|null
      */
     protected function reorderSiblings(array $ordering)
     {
@@ -324,17 +356,35 @@ 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 +428,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 +636,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;
             }

+ 2 - 0
system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php

@@ -15,6 +15,7 @@ use Grav\Common\Grav;
 use Grav\Common\Page\Interfaces\PageCollectionInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Pages;
+use Grav\Common\Uri;
 use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use RuntimeException;
@@ -97,6 +98,7 @@ trait PageRoutableTrait
     public function activeChild(): bool
     {
         $grav = Grav::instance();
+        /** @var Uri $uri */
         $uri = $grav['uri'];
         /** @var Pages $pages */
         $pages = $grav['pages'];

+ 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 ($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,

+ 11 - 0
system/src/Grav/Common/Page/Medium/AbstractMedia.php

@@ -198,6 +198,17 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
         }
     }
 
+    /**
+     * @param string $name
+     * @return void
+     */
+    public function hide($name)
+    {
+        $this->offsetUnset($name);
+
+        unset($this->images[$name], $this->videos[$name], $this->audios[$name], $this->files[$name]);
+    }
+
     /**
      * Create Medium from a file.
      *

+ 13 - 11
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)
     {
@@ -2496,21 +2496,23 @@ class Page implements PageInterface
      */
     public function activeChild()
     {
-        $uri = Grav::instance()['uri'];
-        $pages = Grav::instance()['pages'];
+        $grav = Grav::instance();
+        /** @var Uri $uri */
+        $uri = $grav['uri'];
+        /** @var Pages $pages */
+        $pages = $grav['pages'];
         $uri_path = rtrim(urldecode($uri->path()), '/');
-        $routes = Grav::instance()['pages']->routes();
+        $routes = $pages->routes();
 
         if (isset($routes[$uri_path])) {
+            $page = $pages->find($uri->route());
             /** @var PageInterface|null $child_page */
-            $child_page = $pages->find($uri->route())->parent();
-            if ($child_page) {
-                while (!$child_page->root()) {
-                    if ($this->path() === $child_page->path()) {
-                        return true;
-                    }
-                    $child_page = $child_page->parent();
+            $child_page = $page ? $page->parent() : null;
+            while ($child_page && !$child_page->root()) {
+                if ($this->path() === $child_page->path()) {
+                    return true;
                 }
+                $child_page = $child_page->parent();
             }
         }
 

+ 144 - 79
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();
                 }
@@ -865,103 +880,146 @@ class Pages
     }
 
     /**
-     * alias method to return find a page.
+     * Find a page based on route.
      *
-     * @param string $route The relative URL of the page
-     * @param bool   $all
+     * @param string $route The route of the page
+     * @param bool   $all   If true, return also non-routable pages, otherwise return null if page isn't routable
      * @return PageInterface|null
      */
     public function find($route, $all = false)
     {
-        return $this->dispatch($route, $all, false);
+        $route = urldecode((string)$route);
+
+        // Fetch page if there's a defined route to it.
+        $path = $this->routes[$route] ?? null;
+        $page = null !== $path ? $this->get($path) : null;
+
+        // Try without trailing slash
+        if (null === $page && Utils::endsWith($route, '/')) {
+            $path = $this->routes[rtrim($route, '/')] ?? null;
+            $page = null !== $path ? $this->get($path) : null;
+        }
+
+        if (!$all && !isset($this->grav['admin'])) {
+            if (null === $page || !$page->routable()) {
+                // If the page cannot be accessed, look for the site wide routes and wildcards.
+                $page = $this->findSiteBasedRoute($route) ?? $page;
+            }
+        }
+
+        return $page;
+    }
+
+    /**
+     * Check site based routes.
+     *
+     * @param string $route
+     * @return PageInterface|null
+     */
+    protected function findSiteBasedRoute($route)
+    {
+        /** @var Config $config */
+        $config = $this->grav['config'];
+
+        $site_routes = $config->get('site.routes');
+        if (!is_array($site_routes)) {
+            return null;
+        }
+
+        $page = null;
+
+        // See if route matches one in the site configuration
+        $site_route = $site_routes[$route] ?? null;
+        if ($site_route) {
+            $page = $this->find($site_route);
+        } else {
+            // Use reverse order because of B/C (previously matched multiple and returned the last match).
+            foreach (array_reverse($site_routes, true) as $pattern => $replace) {
+                $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
+                try {
+                    $found = preg_replace($pattern, $replace, $route);
+                    if ($found && $found !== $route) {
+                        $page = $this->find($found);
+                        if ($page) {
+                            return $page;
+                        }
+                    }
+                } catch (ErrorException $e) {
+                    $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
+                }
+            }
+        }
+
+        return $page;
     }
 
     /**
      * Dispatch URI to a page.
      *
      * @param string $route The relative URL of the page
-     * @param bool $all
-     * @param bool $redirect
+     * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable
+     * @param bool $redirect If true, allow redirects
      * @return PageInterface|null
      * @throws Exception
      */
     public function dispatch($route, $all = false, $redirect = true)
     {
-        $route = urldecode($route);
+        $page = $this->find($route, true);
 
-        // Fetch page if there's a defined route to it.
-        $path = $this->routes[$route] ?? null;
-        $page = null !== $path ? $this->get($path) : null;
-        // Try without trailing slash
-        if (!$page && Utils::endsWith($route, '/')) {
-            $path = $this->routes[rtrim($route, '/')] ?? null;
-            $page = null !== $path ? $this->get($path) : null;
+        // If we want all pages or are in admin, return what we already have.
+        if ($all || isset($this->grav['admin'])) {
+            return $page;
         }
 
-        // Are we in the admin? this is important!
-        $not_admin = !isset($this->grav['admin']);
+        if ($page) {
+            $routable = $page->routable();
+            if ($redirect) {
+                if ($page->redirect()) {
+                    // Follow a redirect page.
+                    $this->grav->redirectLangSafe($page->redirect());
+                }
+
+                if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) {
+                    // Redirect to the first visible child as current page isn't routable.
+                    $this->grav->redirectLangSafe($child->route());
+                }
+            }
 
-        // If the page cannot be reached, look into site wide redirects, routes + wildcards
-        if (!$all && $not_admin) {
-            // If the page is a simple redirect, just do it.
-            if ($redirect && $page && $page->redirect()) {
-                $this->grav->redirectLangSafe($page->redirect());
+            if ($routable) {
+                return $page;
             }
+        }
 
-            // fall back and check site based redirects
-            if (!$page || !$page->routable()) {
-                // Redirect to the first child (placeholder page)
-                if ($redirect && $page && count($children = $page->children()->visible()->routable()->published()) > 0) {
-                    $this->grav->redirectLangSafe($children->first()->route());
-                }
+        $route = urldecode((string)$route);
 
-                /** @var Config $config */
-                $config = $this->grav['config'];
+        // The page cannot be reached, look into site wide redirects, routes and wildcards.
+        $redirectedPage = $this->findSiteBasedRoute($route);
+        if ($redirectedPage) {
+            $page = $this->dispatch($redirectedPage->route(), false, $redirect);
+        }
 
-                // See if route matches one in the site configuration
-                $site_route = $config->get("site.routes.{$route}");
-                if ($site_route) {
-                    $page = $this->dispatch($site_route, $all, $redirect);
-                } else {
-                    /** @var Uri $uri */
-                    $uri = $this->grav['uri'];
-                    /** @var \Grav\Framework\Uri\Uri $source_url */
-                    $source_url = $uri->uri(false);
-
-                    // Try Regex style redirects
-                    $site_redirects = $config->get('site.redirects');
-                    if (is_array($site_redirects)) {
-                        foreach ((array)$site_redirects as $pattern => $replace) {
-                            $pattern = ltrim($pattern, '^');
-                            $pattern = '#^' . str_replace('/', '\/', $pattern) . '#';
-                            try {
-                                /** @var string $found */
-                                $found = preg_replace($pattern, $replace, $source_url);
-                                if ($found && $found !== $source_url) {
-                                    $this->grav->redirectLangSafe($found);
-                                }
-                            } catch (ErrorException $e) {
-                                $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
-                            }
-                        }
-                    }
+        /** @var Config $config */
+        $config = $this->grav['config'];
 
-                    // Try Regex style routes
-                    $site_routes = $config->get('site.routes');
-                    if (is_array($site_routes)) {
-                        foreach ((array)$site_routes as $pattern => $replace) {
-                            $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
-                            try {
-                                /** @var string $found */
-                                $found = preg_replace($pattern, $replace, $source_url);
-                                if ($found && $found !== $source_url) {
-                                    $page = $this->dispatch($found, $all, $redirect);
-                                }
-                            } catch (ErrorException $e) {
-                                $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
-                            }
-                        }
+        /** @var Uri $uri */
+        $uri = $this->grav['uri'];
+        /** @var \Grav\Framework\Uri\Uri $source_url */
+        $source_url = $uri->uri(false);
+
+        // Try Regex style redirects
+        $site_redirects = $config->get('site.redirects');
+        if (is_array($site_redirects)) {
+            foreach ((array)$site_redirects as $pattern => $replace) {
+                $pattern = ltrim($pattern, '^');
+                $pattern = '#^' . str_replace('/', '\/', $pattern) . '#';
+                try {
+                    /** @var string $found */
+                    $found = preg_replace($pattern, $replace, $source_url);
+                    if ($found && $found !== $source_url) {
+                        $this->grav->redirectLangSafe($found);
                     }
+                } catch (ErrorException $e) {
+                    $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
                 }
             }
         }
@@ -1159,7 +1217,14 @@ 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

+ 7 - 10
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)
     {
@@ -518,7 +519,7 @@ class Uri
      * Return the full uri
      *
      * @param bool $include_root
-     * @return mixed
+     * @return string
      */
     public function uri($include_root = true)
     {
@@ -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);
     }
 
     /**

+ 112 - 33
system/src/Grav/Common/Utils.php

@@ -12,11 +12,15 @@ namespace Grav\Common;
 use DateTime;
 use DateTimeZone;
 use Exception;
+use Grav\Common\Flex\Types\Pages\PageObject;
 use Grav\Common\Helpers\Truncator;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Markdown\Parsedown;
 use Grav\Common\Markdown\ParsedownExtra;
 use Grav\Common\Page\Markdown\Excerpts;
+use Grav\Common\Page\Pages;
+use Grav\Framework\Flex\Flex;
+use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 use InvalidArgumentException;
 use Negotiation\Accept;
 use Negotiation\Negotiator;
@@ -1520,7 +1524,7 @@ abstract class Utils
     }
 
     /**
-     * Get path based on a token
+     * Get relative page path based on a token.
      *
      * @param string $path
      * @param PageInterface|null $page
@@ -1529,47 +1533,122 @@ abstract class Utils
      */
     public static function getPagePathFromToken($path, PageInterface $page = null)
     {
-        $path_parts = pathinfo($path);
-        $grav       = Grav::instance();
+        return static::getPathFromToken($path, $page);
+    }
 
-        $basename = '';
-        if (isset($path_parts['extension'])) {
-            $basename = '/' . $path_parts['basename'];
-            $path     = rtrim($path_parts['dirname'], ':');
+    /**
+     * Get relative path based on a token.
+     *
+     * Path supports following syntaxes:
+     *
+     * 'self@', 'self@/path'
+     * 'page@:/route', 'page@:/route/filename.ext'
+     * 'theme@:', 'theme@:/path'
+     *
+     * @param string $path
+     * @param FlexObjectInterface|PageInterface|null $object
+     * @return string
+     * @throws RuntimeException
+     */
+    public static function getPathFromToken($path, $object = null)
+    {
+        $matches = static::resolveTokenPath($path);
+        if (null === $matches) {
+            return $path;
         }
 
-        $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
-        preg_match($regex, $path, $matches);
+        $grav = Grav::instance();
 
-        if ($matches) {
-            if ($matches[1]) {
-                if (null === $page) {
-                    throw new RuntimeException('Page not available for this self@ reference');
+        switch ($matches[0]) {
+            case 'self':
+                if (null === $object) {
+                    throw new RuntimeException(sprintf('Page not available for self@ reference: %s', $path));
                 }
-            } elseif ($matches[2]) {
-                // page@
-                $parts = explode(':', $path);
-                $route = $parts[1];
-                $page  = $grav['page']->find($route);
-            } elseif ($matches[3]) {
-                // theme@
-                $parts = explode(':', $path);
-                $route = $parts[1];
-                $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource('theme://'));
-
-                return $theme . $route . $basename;
-            }
-        } else {
-            return $path . $basename;
-        }
 
-        if (!$page) {
-            throw new RuntimeException('Page route not found: ' . $path);
+                if ($matches[2] === '') {
+                    if ($object->exists()) {
+                        $route = '/' . $matches[1];
+
+                        if ($object instanceof PageInterface) {
+                            return trim($object->relativePagePath() . $route, '/');
+                        }
+
+                        $folder = $object->getMediaFolder();
+                        if ($folder) {
+                            return trim($folder . $route, '/');
+                        }
+                    } else {
+                        return '';
+                    }
+                }
+
+                break;
+            case 'page':
+                if ($matches[1] === '') {
+                    $route = '/' . $matches[2];
+
+                    // Exclude filename from the page lookup.
+                    if (pathinfo($route, PATHINFO_EXTENSION)) {
+                        $basename = '/' . basename($route);
+                        $route = \dirname($route);
+                    } else {
+                        $basename = '';
+                    }
+
+                    $key = trim($route === '/' ? $grav['config']->get('system.home.alias') : $route, '/');
+                    if ($object instanceof PageObject) {
+                        $object = $object->getFlexDirectory()->getObject($key);
+                    } elseif (static::isAdminPlugin()) {
+                        /** @var Flex|null $flex */
+                        $flex = $grav['flex'] ?? null;
+                        $object = $flex ? $flex->getObject($key, 'pages') : null;
+                    } else {
+                        /** @var Pages $pages */
+                        $pages = $grav['pages'];
+                        $object = $pages->find($route);
+                    }
+
+                    if ($object instanceof PageInterface) {
+                        return trim($object->relativePagePath() . $basename, '/');
+                    }
+                }
+
+                break;
+            case 'theme':
+                if ($matches[1] === '') {
+                    $route = '/' . $matches[2];
+                    $theme = $grav['locator']->findResource('theme://', false);
+                    if (false !== $theme) {
+                        return trim($theme . $route, '/');
+                    }
+                }
+
+                break;
         }
 
-        $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
+        throw new RuntimeException(sprintf('Token path not found: %s', $path));
+    }
+
+    /**
+     * Returns [token, route, path] from '@token/route:/path'. Route and path are optional. If pattern does not match, return null.
+     *
+     * @param string $path
+     * @return string[]|null
+     */
+    private static function resolveTokenPath(string $path): ?array
+    {
+        if (strpos($path, '@') !== false) {
+            $regex = '/^(@\w+|\w+@|@\w+@)([^:]*)(.*)$/u';
+            if (preg_match($regex, $path, $matches)) {
+                return [
+                    trim($matches[1], '@'),
+                    trim($matches[2], '/'),
+                    trim($matches[3], ':/')
+                ];
+            }
+        }
 
-        return $path . $basename;
+        return null;
     }
 
     /**

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

+ 24 - 18
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,26 +416,29 @@ trait PageRoutableTrait
             throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
         }
 
-        if ($this->root()) {
-            return null;
+        if ($this->_parentCache || $this->root()) {
+            return $this->_parentCache;
         }
 
+        // Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'.
         $filesystem = Filesystem::getInstance(false);
         $directory = $this->getFlexDirectory();
         $parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/');
-        if ($parentKey) {
+        if ('' !== $parentKey) {
             $parent = $directory->getObject($parentKey);
             $language = $this->getLanguage();
             if ($language && $parent && method_exists($parent, 'getTranslation')) {
                 $parent = $parent->getTranslation($language) ?? $parent;
             }
 
-            return $parent;
-        }
+            $this->_parentCache = $parent;
+        } else {
+            $index = $directory->getIndex();
 
-        $index = $directory->getIndex();
+            $this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null;
+        }
 
-        return method_exists($index, 'getRoot') ? $index->getRoot() : null;
+        return $this->_parentCache;
     }
 
     /**
@@ -493,22 +499,22 @@ trait PageRoutableTrait
     public function activeChild(): bool
     {
         $grav = Grav::instance();
+        /** @var Uri $uri */
         $uri = $grav['uri'];
+        /** @var Pages $pages */
         $pages = $grav['pages'];
         $uri_path = rtrim(urldecode($uri->path()), '/');
         $routes = $pages->routes();
 
         if (isset($routes[$uri_path])) {
-            /** @var PageInterface $child_page|null */
-            $child_page = $pages->find($uri->route())->parent();
-            if (null !== $child_page) {
-                while (!$child_page->root()) {
-                    if ($this->path() === $child_page->path()) {
-                        return true;
-                    }
-                    /** @var PageInterface $child_page|null */
-                    $child_page = $child_page->parent();
+            $page = $pages->find($uri->route());
+            /** @var PageInterface|null $child_page */
+            $child_page = $page ? $page->parent() : null;
+            while ($child_page && !$child_page->root()) {
+                if ($this->path() === $child_page->path()) {
+                    return true;
                 }
+                $child_page = $child_page->parent();
             }
         }
 

+ 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 = [];

+ 69 - 13
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php

@@ -14,8 +14,10 @@ use Grav\Common\Grav;
 use Grav\Common\Media\Interfaces\MediaCollectionInterface;
 use Grav\Common\Media\Interfaces\MediaUploadInterface;
 use Grav\Common\Media\Traits\MediaTrait;
+use Grav\Common\Page\Media;
 use Grav\Common\Page\Medium\Medium;
 use Grav\Common\Page\Medium\MediumFactory;
+use Grav\Common\Utils;
 use Grav\Framework\Cache\CacheInterface;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\FlexDirectory;
@@ -23,6 +25,7 @@ use Grav\Framework\Form\FormFlashFile;
 use Psr\Http\Message\UploadedFileInterface;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RuntimeException;
+use function array_key_exists;
 use function in_array;
 use function is_array;
 use function is_callable;
@@ -75,11 +78,40 @@ trait FlexMediaTrait
         return $media;
     }
 
+    /**
+     * @param string $field
+     * @return MediaCollectionInterface|null
+     */
+    public function getMediaField(string $field): ?MediaCollectionInterface
+    {
+        // Field specific media.
+        $settings = $this->getFieldSettings($field);
+        if (!empty($settings['media_field'])) {
+            $var = 'destination';
+        } elseif (!empty($settings['media_picker_field'])) {
+            $var = 'folder';
+        }
+
+        if (empty($var)) {
+            // Not a media field.
+            $media = null;
+        } elseif ($settings['self']) {
+            // Uses main media.
+            $media = $this->getMedia();
+        } else {
+            // Uses custom media.
+            $media = new Media($settings[$var]);
+            $this->addUpdatedMedia($media);
+        }
+
+        return $media;
+    }
+
     /**
      * @param string $field
      * @return array|null
      */
-    protected function getFieldSettings(string $field): ?array
+    public function getFieldSettings(string $field): ?array
     {
         if ($field === '') {
             return null;
@@ -88,14 +120,32 @@ trait FlexMediaTrait
         // Load settings for the field.
         $schema = $this->getBlueprint()->schema();
         $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
+        if (!isset($settings) || !is_array($settings)) {
+            return null;
+        }
 
-        if (isset($settings['type']) && (in_array($settings['type'], ['avatar', 'file', 'pagemedia']) || !empty($settings['destination']))) {
-            // Set destination folder.
+        $type = $settings['type'] ?? '';
+
+        // Media field.
+        if (!empty($settings['media_field']) || array_key_exists('destination', $settings) || in_array($type, ['avatar', 'file', 'pagemedia'], true)) {
             $settings['media_field'] = true;
-            if (empty($settings['destination']) || in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) {
-                $settings['destination'] = $this->getMediaFolder();
+            $var = 'destination';
+        }
+
+        // Media picker field.
+        if (!empty($settings['media_picker_field']) || in_array($type, ['filepicker', 'pagemediaselect'], true)) {
+            $settings['media_picker_field'] = true;
+            $var = 'folder';
+        }
+
+        // Set media folder for media fields.
+        if (isset($var)) {
+            $folder = $settings[$var] ?? '';
+            if (in_array(rtrim($folder, '/'), ['', '@self', 'self@', '@self@'], true)) {
+                $settings[$var] = $this->getMediaFolder();
                 $settings['self'] = true;
             } else {
+                $settings[$var] = Utils::getPathFromToken($folder, $this);
                 $settings['self'] = false;
             }
         }
@@ -115,7 +165,6 @@ trait FlexMediaTrait
         return $settings + ['accept' => '*', 'limit' => 1000, 'self' => true];
     }
 
-
     protected function getMediaFields(): array
     {
         // Load settings for the field.
@@ -206,12 +255,13 @@ trait FlexMediaTrait
      */
     public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null): void
     {
-        $media = $this->getMedia();
+        $settings = $this->getMediaFieldSettings($field ?? '');
+
+        $media = $field ? $this->getMediaField($field) : $this->getMedia();
         if (!$media instanceof MediaUploadInterface) {
             throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads.");
         }
 
-        $settings = $this->getMediaFieldSettings($field ?? '');
         $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
         $media->copyUploadedFile($uploadedFile, $filename, $settings);
         $this->clearMediaCache();
@@ -322,13 +372,20 @@ trait FlexMediaTrait
         foreach ($this->getUpdatedMedia() as $filename => $upload) {
             if (is_array($upload)) {
                 // Uses new format with [UploadedFileInterface, array].
-                $upload = $upload[0];
+                $settings = $upload[1];
+                if ($settings['destination'] === $media->getPath()) {
+                    $upload = $upload[0];
+                } else {
+                    $upload = false;
+                }
             }
-            if ($upload) {
-                $medium = MediumFactory::fromUploadedFile($upload);
+            if (false !== $upload) {
+                $medium = $upload ? MediumFactory::fromUploadedFile($upload) : null;
+                $updated = true;
                 if ($medium) {
-                    $updated = true;
                     $media->add($filename, $medium);
+                } else {
+                    $media->hide($filename);
                 }
             }
         }
@@ -356,7 +413,6 @@ trait FlexMediaTrait
             return;
         }
 
-
         // Upload/delete altered files.
         /**
          * @var string $filename

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

+ 1 - 1
user/blueprints/pages/modular/personnes.yaml

@@ -40,5 +40,5 @@ form:
                   type: markdown
                   label: Biographie
                 .portrait:
-                  type: file
+                  type: filepicker
                   label: Portrait

+ 14 - 11
user/blueprints/pages/modular/programmes.yaml

@@ -19,22 +19,25 @@ form:
                 header.template:
                   default: modular/programmes
                   '@data-options': '\Grav\Common\Page\Pages::modularTypes'
-        features:
+        programmes:
           type: tab
           title: Programmes
           fields:
-            header.features:
-              name: features
+            header.programmes:
+              name: programmes
               type: list
               label: Programmes
 
               fields:
-                .icon:
+                .nom_du_programme:
                   type: text
-                  label: Icon
-                .header:
-                  type: text
-                  label: Header
-                .text:
-                  type: text
-                  label: Text
+                  label: Nom du programme
+                .texte_de_presentation:
+                  type: markdown
+                  label: Texte de présentation du programme
+                  validate:
+                    type: textarea
+                .logo:
+                  name: logo_du_programme
+                  type: filepicker
+                  label: Logo

+ 48 - 0
user/blueprints/pages/modular/rapport_dactivitees.yaml

@@ -0,0 +1,48 @@
+title: Rapport d'activitées
+'@extends': default
+
+form:
+  fields:
+    tabs:
+      fields:
+        advanced:
+          fields:
+            columns:
+              fields:
+                column1:
+                  fields:
+                    name:
+                      default: modular/rapport_dactivitees
+                      '@data-options': '\Grav\Common\Page\Pages::modularTypes'
+            overrides:
+              fields:
+                header.template:
+                  default: modular/rapport_dactivitees
+                  '@data-options': '\Grav\Common\Page\Pages::modularTypes'
+        rapports:
+          type: tab
+          title: Rapports d'activitées
+          fields:
+            header.rapports:
+              name: rapports
+              type: list
+              label: Rapports d'activitées
+
+              fields:
+                .titre_du_rapport:
+                  type: text
+                  label: Titre du rapport
+                .texte_de_presentation:
+                  type: markdown
+                  label: Texte de présentation du rapport
+                  validate:
+                    type: textarea
+                .couverture:
+                  name: couv_du_rapport
+                  type: filepicker
+                  label: Image de couverture
+                .pdf:
+                  type: filepicker
+                  label: pdf à télécharger
+                  accept:
+                    - .pdf

+ 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 Chiadmi'
+  email: ouidade@figureslibres.io
 taxonomies:
   - category
   - tag

+ 11 - 12
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
@@ -45,17 +43,17 @@ pages:
   publish_dates: true
   process:
     markdown: true
-    twig: false
+    twig: true
   twig_first: false
-  never_cache_twig: false
+  never_cache_twig: true
   events:
     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:
@@ -98,7 +96,7 @@ pages:
 cache:
   enabled: true
   check:
-    method: file
+    method: hash
   driver: auto
   prefix: g
   purge_at: '0 4 * * *'
@@ -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:

+ 2 - 2
user/pages/01.home/01._accueil/text.md

@@ -5,9 +5,9 @@ visible: true
 debugger: true
 ---
 
-# GIP EPAU
+# EPAU
 
 
-## GROUPEMENT D'INTÉRÊT PUBLIC L'EUROPE DES PROJETS ARCHITECTURAUX ET URBAINS
+## L'EUROPE DES PROJETS ARCHITECTURAUX ET URBAINS
 
 Organisme de recherche et d’expérimentation opérant des programmes nationaux sous tutelles des Ministères en charge de l’architecture et de l’urbanisme, la Cité de l’architecture et du patrimoine et l’Association Europan France.

BIN
user/pages/01.home/02._programmes/10361_web_01.jpg


BIN
user/pages/01.home/02._programmes/14360_default_big.jpg


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


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 7
user/pages/01.home/02._programmes/features.md


BIN
user/pages/01.home/02._programmes/hyperliens_marseille.PNG


BIN
user/pages/01.home/02._programmes/logo_coubertin.png


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


Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
user/pages/01.home/02._programmes/programmes.md


BIN
user/pages/01.home/03._rapports-dactivitees/rapport_activite_V1.pdf


BIN
user/pages/01.home/03._rapports-dactivitees/rapport_activite_V1.png


+ 39 - 0
user/pages/01.home/03._rapports-dactivitees/rapport_dactivitees.md

@@ -0,0 +1,39 @@
+---
+title: Ressources
+body_classes: modular
+media_order: 'rapport_activite_V1.pdf,rapport_activite_V1.png,rapport_activite_V1.pdf'
+visible: true
+debugger: true
+rapports:
+    -
+        titre_du_rapport: 'rapport d''activité 2020'
+        texte_de_presentation: "Courte explicationVita estillis semper in fuga uxoresque mercenariae\r\n infugauxoresque mercenariae conductae ad tempus expacto atque, ut conductae ad tempus ex pacto atque, ut sit species matrimonii, dotis nomine futura coniunx hastam.\r\n"
+        couverture: rapport_activite_V1.png
+        pdf: rapport_activite_V1.pdf
+    -
+        titre_du_rapport: 'rapport 3'
+        texte_de_presentation: "Courte explicationVita estillis semper in fuga uxoresque mercenariae\r\n infugauxoresque mercenariae conductae ad tempus expacto atque, ut conductae ad tempus ex pacto atque, ut sit species matrimonii, dotis nomine futura coniunx hastam.\r\n"
+        couverture: rapport_activite_V1.png
+        pdf: rapport_activite_V1.pdf
+    -
+        titre_du_rapport: 'exemple 3'
+        texte_de_presentation: "Courte explicationVita estillis semper in fuga uxoresque mercenariae\r\n infugauxoresque mercenariae conductae ad tempus expacto atque, ut conductae ad tempus ex pacto atque, ut sit species matrimonii, dotis nomine futura coniunx hastam.\r\n"
+        couverture: rapport_activite_V1.png
+        pdf: rapport_activite_V1.pdf
+    -
+        titre_du_rapport: 'exemple 4'
+        texte_de_presentation: "Courte explicationVita estillis semper in fuga uxoresque mercenariae\r\n infugauxoresque mercenariae conductae ad tempus expacto atque, ut conductae ad tempus ex pacto atque, ut sit species matrimonii, dotis nomine futura coniunx hastam.\r\n"
+        couverture: rapport_activite_V1.png
+        pdf: rapport_activite_V1.pdf
+    -
+        titre_du_rapport: 'exempl 5'
+        texte_de_presentation: "Courte explicationVita estillis semper in fuga uxoresque mercenariae\r\n infugauxoresque mercenariae conductae ad tempus expacto atque, ut conductae ad tempus ex pacto atque, ut sit species matrimonii, dotis nomine futura coniunx hastam.\r\n"
+        couverture: rapport_activite_V1.png
+        pdf: rapport_activite_V1.pdf
+template: modular/rapport_dactivitees
+process:
+    markdown: true
+    twig: false
+---
+
+### Ressources

BIN
user/pages/01.home/03._rapports-dactivites/images.jpeg


+ 0 - 13
user/pages/01.home/03._rapports-dactivites/showcase.md

@@ -1,13 +0,0 @@
----
-title: 'Rapports d''activités'
-body_classes: modular
-media_order: images.jpeg
-visible: true
-debugger: true
----
-
-###  Rapports d’activités de l’Europe des projets architecturaux et urbains 
-
-
-[Lien pour télécharger]() 
-![]images.jpeg(http://)+ images couv

+ 19 - 0
user/pages/01.home/04._gouvernance/01._conseil-dadministration/01._presidence/personnes.md

@@ -0,0 +1,19 @@
+---
+title: Présidence
+visible: true
+body_classes: 'modular-row personnes'
+personnes:
+    -
+        nom: 'Hélène Peskine'
+        fonction: 'Présidente du Conseil d’administration'
+        biographie: 'Secrétaire permanente du Plan Urbanisme Construction Architecture, Ministère de la Transition écologique, Ministère de la Cohésion des territoires et des Relations avec les Collectivités territoriales'
+        portrait: null
+admin: {  }
+process:
+    markdown: true
+    twig: true
+debugger: true
+template: modular/personnes
+---
+
+### Présidence

+ 16 - 0
user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md

@@ -0,0 +1,16 @@
+---
+title: 'Membres du conseil d''administration'
+visible: true
+body_classes: 'modular-row personnes'
+personnes:
+    -
+        nom: 'Jean-Baptiste Butlen'
+        fonction: 'Membre du Conseil d’administration'
+        biographie: 'Sous-directeur Aménagement durable, Direction Générale de l’Aménagement, du Logement et de la Nature, Ministère de la Transition écologique, Ministère de la Cohésion des territoires et des Relations avec les Collectivités territoriales'
+        portrait: photo_butlen.jpeg
+debugger: true
+template: modular/personnes
+media_order: photo_butlen.jpeg
+---
+
+### Membres du conseil d'administration

+ 0 - 0
user/pages/01.home/04._gouvernance/photo_butlen.jpeg → user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/photo_butlen.jpeg


+ 20 - 0
user/pages/01.home/04._gouvernance/01._conseil-dadministration/conseil_dadministration.md

@@ -0,0 +1,20 @@
+---
+title: 'Conseil d''dadministration'
+menu: 'Conseil d''dadministration'
+visible: true
+onpage_menu: true
+body_classes: 'modular fullwidth title-center title-h1h2'
+content:
+    items: '@self.modular'
+    order:
+        by: default
+        dir: asc
+        custom:
+            - _presidence
+            - _membres-du-conseil-dadministration
+admin:
+    children_display_order: collection
+debugger: true
+---
+
+## Conseil d'administration

BIN
user/pages/01.home/04._gouvernance/02._equipe-epau/Alain Maugard_TROUVE.jpg


Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 17
user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md


+ 20 - 0
user/pages/01.home/04._gouvernance/gouvernance.md

@@ -0,0 +1,20 @@
+---
+title: Gouvernance
+menu: Gouvernance
+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
+            - _equipe-epau
+admin:
+    children_display_order: collection
+debugger: true
+---
+
+# Gouvernance

Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
user/pages/01.home/EPAU_logo_EPAU_vecto-seul.svg


BIN
user/pages/01.home/Logotype_GIP_EPAU.svg.png


Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
user/pages/01.home/Republique-francaise-logo.svg


+ 2 - 2
user/pages/01.home/modular.md

@@ -11,9 +11,9 @@ content:
         custom:
             - _accueil
             - _programmes
-            - _rapports-dactivites
+            - _rapports-dactivitees
             - _gouvernance
             - _contact
-media_order: 'jeux-olypiques-paris-2024-village-athle-lot-e2.jpg,Logotype_GIP_EPAU.svg.png'
+media_order: 'EPAU_logo_EPAU_vecto-seul.svg,Republique-francaise-logo.svg,popsu.png'
 ---
 

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


+ 0 - 19
user/pages/02.programmes/01.popsu/default.md

@@ -1,19 +0,0 @@
----
-title: 'Plateforme d’observation des projets et stratégies urbaine'
-media_order: POPSU2018_LOGO_moinsde6cm_sscartouche.png
-content:
-    items: '@self.modular'
-visible: false
----
-
-Lieu de convergence des milieux de la recherche
-et de ceux qui font et gouvernent les villes, la
-Plateforme d’observation des projets et
-stratégies urbaines – POPSU – croise les savoirs
-scientifiques et l’expertise opérationnelle pour
-mieux comprendre les enjeux et les évolutions
-associés aux villes et aux territoires. Elle
-capitalise, à des fins d’action, les connaissances
-établies sur les métropoles et en assure la
-diffusion auprès de publics divers :
-professionnels, académiques, grand public...

BIN
user/pages/02.programmes/02.european/Europan France.jpg


+ 0 - 18
user/pages/02.programmes/02.european/default.md

@@ -1,18 +0,0 @@
----
-title: European
-media_order: 'Europan France.jpg'
-visible: false
----
-
-# Europan
-
-Europan est un concours d’idées d’architecture
-et d’urbanisme se déroulant tous les deux ans
-simultanément dans une vingtaine de pays
-européens, sur un thème et un règlement
-communs. Dans chaque pays des sites sont
-proposés par des collectivités territoriales et
-leurs partenaires. Ce concours est suivi de
-réalisation in-situ pour les équipes sélectionnées.
-Europan facilite le dialogue sur les modes de
-faire la ville à toutes les échelles.

+ 0 - 16
user/pages/02.programmes/03.coubertin/item.md

@@ -1,16 +0,0 @@
----
-title: Coubertin
-visible: false
----
-
-# Coubertin
-
-Le programme de recherche-action Coubertin
-construit le récit, au fil de l’eau, de la conception
-des ouvrages et des opérations d’aménagement
-des Jeux Olympiques et Paralympiques de Paris
-2024. À travers une observation embarquée au
-sein de la SOLIDEO, l’équipe de chercheurs
-analyse la production architecturale et urbaine et
-sa capacité à transformer les pratiques
-d’aménagement.

BIN
user/pages/02.programmes/04.forum-des-solutions/forum_solution.png


+ 0 - 20
user/pages/02.programmes/04.forum-des-solutions/item.md

@@ -1,20 +0,0 @@
----
-title: 'Forum des Solutions'
-media_order: forum_solution.png
-visible: false
----
-
-# Forum des Solutions
-
-Prolongement physique des programmes Action
-Cœur de Ville et Petites villes de demain, le
-Forum des solutions est une série de rendez-vous
-mensuels et thématiques conçues pour les villes
-du programme, mais qui s’adresse également à
-toutes les collectivités qui souhaitent placer
-l’innovation au cœur des stratégies de
-revitalisation urbaine. Chaque rendez-vous
-permet la présentation par leurs auteurs de
-projets réalisés répondant aux problématiques
-rencontrées par ces villes. Il est ouvert à tous les
-acteurs de l’aménagement du territoire.

+ 0 - 5
user/pages/02.programmes/05.rapports-dactivites/default.md

@@ -1,5 +0,0 @@
----
-title: 'Rapports d''activités'
-visible: false
----
-

+ 0 - 17
user/pages/02.programmes/modular.md

@@ -1,17 +0,0 @@
----
-title: Programmes
-menu: Programmes
-onpage_menu: true
-body_classes: 'modular header-image fullwidth'
-content:
-    items: '@self.modular'
-    order:
-        by: default
-        dir: asc
-        custom:
-            - _popsu
-            - _european
-published: true
-visible: false
----
-

+ 0 - 7
user/pages/03.gouvernance/01.conseil-dadministration/item.md

@@ -1,7 +0,0 @@
----
-title: 'Conseil d''administration de l''Europe des projets arcitecturaux et urbains'
-content:
-    items: '@self.modular'
-visible: false
----
-

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff