ouidade преди 4 години
родител
ревизия
9494467cc2
променени са 100 файла, в които са добавени 3814 реда и са изтрити 760 реда
  1. 139 0
      CHANGELOG.md
  2. 0 1
      CODE_OF_CONDUCT.md
  3. 6 6
      SECURITY.md
  4. 189 239
      composer.lock
  5. 7 0
      fixperms.sh
  6. 2 2
      images/.gitkeep
  7. 2 1
      logs/.gitkeep
  8. 1 1
      system/blueprints/config/security.yaml
  9. 1 1
      system/blueprints/flex/accounts.yaml
  10. 6 2
      system/blueprints/flex/shared/configure.yaml
  11. 1986 0
      system/config/mime.yaml
  12. 2 2
      system/config/permissions.yaml
  13. 1 1
      system/defines.php
  14. 11 0
      system/images/media/thumb-3dm.png
  15. 16 0
      system/languages/bg.yaml
  16. 9 5
      system/languages/cs.yaml
  17. 3 0
      system/languages/gl.yaml
  18. 81 31
      system/languages/he.yaml
  19. 147 0
      system/languages/is.yaml
  20. 3 0
      system/languages/nb.yaml
  21. 9 0
      system/languages/ro.yaml
  22. 2 0
      system/languages/sk.yaml
  23. 16 1
      system/languages/uk.yaml
  24. 21 7
      system/languages/zh.yaml
  25. 17 7
      system/src/Grav/Common/Assets/BaseAsset.php
  26. 18 13
      system/src/Grav/Common/Assets/Css.php
  27. 6 4
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  28. 1 1
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  29. 2 6
      system/src/Grav/Common/Browser.php
  30. 13 3
      system/src/Grav/Common/Composer.php
  31. 2 2
      system/src/Grav/Common/Data/Blueprint.php
  32. 13 0
      system/src/Grav/Common/Data/BlueprintSchema.php
  33. 1 1
      system/src/Grav/Common/Data/Blueprints.php
  34. 23 8
      system/src/Grav/Common/Data/DataInterface.php
  35. 1 1
      system/src/Grav/Common/Data/ValidationException.php
  36. 4 3
      system/src/Grav/Common/Errors/BareHandler.php
  37. 3 4
      system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
  38. 3 2
      system/src/Grav/Common/Flex/FlexCollection.php
  39. 19 12
      system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
  40. 20 13
      system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
  41. 46 12
      system/src/Grav/Common/Flex/Types/Pages/PageObject.php
  42. 4 1
      system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
  43. 8 0
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
  44. 2 2
      system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php
  45. 2 2
      system/src/Grav/Common/Flex/Types/Users/UserIndex.php
  46. 39 5
      system/src/Grav/Common/Flex/Types/Users/UserObject.php
  47. 99 37
      system/src/Grav/Common/Form/FormFlash.php
  48. 19 13
      system/src/Grav/Common/GPM/Installer.php
  49. 6 1
      system/src/Grav/Common/GravTrait.php
  50. 12 10
      system/src/Grav/Common/Helpers/Exif.php
  51. 1 3
      system/src/Grav/Common/Helpers/Truncator.php
  52. 2 11
      system/src/Grav/Common/Language/Language.php
  53. 1 1
      system/src/Grav/Common/Markdown/Parsedown.php
  54. 23 17
      system/src/Grav/Common/Media/Traits/MediaFileTrait.php
  55. 27 13
      system/src/Grav/Common/Media/Traits/StaticResizeTrait.php
  56. 5 5
      system/src/Grav/Common/Page/Header.php
  57. 1 1
      system/src/Grav/Common/Page/Interfaces/PageContentInterface.php
  58. 3 2
      system/src/Grav/Common/Page/Media.php
  59. 32 0
      system/src/Grav/Common/Page/Medium/AbstractMedia.php
  60. 10 7
      system/src/Grav/Common/Page/Medium/Link.php
  61. 2 2
      system/src/Grav/Common/Page/Medium/Medium.php
  62. 76 14
      system/src/Grav/Common/Page/Pages.php
  63. 21 0
      system/src/Grav/Common/Page/Traits/PageFormTrait.php
  64. 26 17
      system/src/Grav/Common/Plugins.php
  65. 3 3
      system/src/Grav/Common/Processors/AssetsProcessor.php
  66. 20 3
      system/src/Grav/Common/Processors/PagesProcessor.php
  67. 12 7
      system/src/Grav/Common/Processors/PluginsProcessor.php
  68. 4 2
      system/src/Grav/Common/Processors/RequestProcessor.php
  69. 16 8
      system/src/Grav/Common/Scheduler/Scheduler.php
  70. 14 0
      system/src/Grav/Common/Service/AccountsServiceProvider.php
  71. 11 2
      system/src/Grav/Common/Service/ErrorServiceProvider.php
  72. 10 6
      system/src/Grav/Common/Session.php
  73. 1 1
      system/src/Grav/Common/Taxonomy.php
  74. 23 17
      system/src/Grav/Common/Theme.php
  75. 19 0
      system/src/Grav/Common/Twig/Exception/TwigException.php
  76. 42 2
      system/src/Grav/Common/Twig/Extension/FilesystemExtension.php
  77. 51 13
      system/src/Grav/Common/Twig/Extension/GravExtension.php
  78. 1 1
      system/src/Grav/Common/Twig/Node/TwigNodeCache.php
  79. 7 8
      system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php
  80. 68 46
      system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php
  81. 37 30
      system/src/Grav/Common/Twig/TwigClockworkDataSource.php
  82. 24 13
      system/src/Grav/Common/User/Access.php
  83. 4 5
      system/src/Grav/Common/Yaml.php
  84. 1 1
      system/src/Grav/Console/Cli/ClearCacheCommand.php
  85. 8 1
      system/src/Grav/Framework/Collection/AbstractIndexCollection.php
  86. 1 1
      system/src/Grav/Framework/Collection/AbstractLazyCollection.php
  87. 1 1
      system/src/Grav/Framework/Collection/ArrayCollection.php
  88. 1 1
      system/src/Grav/Framework/Collection/CollectionInterface.php
  89. 1 1
      system/src/Grav/Framework/Collection/FileCollection.php
  90. 1 1
      system/src/Grav/Framework/Collection/FileCollectionInterface.php
  91. 41 5
      system/src/Grav/Framework/Compat/Serializable.php
  92. 18 3
      system/src/Grav/Framework/Flex/FlexDirectoryForm.php
  93. 39 7
      system/src/Grav/Framework/Flex/FlexForm.php
  94. 5 1
      system/src/Grav/Framework/Flex/FlexFormFlash.php
  95. 48 7
      system/src/Grav/Framework/Flex/FlexObject.php
  96. 1 1
      system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php
  97. 2 2
      system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php
  98. 1 0
      system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php
  99. 4 4
      system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php
  100. 1 0
      system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php

+ 139 - 0
CHANGELOG.md

@@ -1,3 +1,142 @@
+# v1.7.23
+## 09/29/2021
+
+1. [](#new)
+    * Added method `Pages::referrerRoute()` to get the referrer route and language
+    * Added true unique `Utils::uniqueId()` / `{{ unique_id() }}` utilities  with length, prefix, and suffix support
+2. [](#improved)
+   * Replaced GPL `SVG-Sanitizer` with MIT licensed `DOM-Sanitizer`
+   * `Uri::referrer()` now accepts third parameter, if set to `true`, it returns route without base or language code [#3411](https://github.com/getgrav/grav/issues/3411)
+   * Updated vendor libs with latest
+   * Updated with latest language strings via Crowdin.com
+3. [](#bugfix)
+    * Fixed `Folder::move()` throwing an error when target folder is changed by only appending characters to the end [#3445](https://github.com/getgrav/grav/issues/3445)
+    * Fixed some phpstan issues (all code back to level 1, Framework level 3)
+    * Fixed form reset causing image uploads to fail when using Flex
+
+# v1.7.22
+## 09/16/2021
+
+1. [](#new)
+    * Register plugin autoloaders into plugin objects
+2. [](#improved)
+    * Improve Twig 2 compatibility
+    * Update to customized version of Twig DeferredExtension (Twig 1/2 compatible)
+3. [](#bugfix)
+    * Fixed conflicting `$_original` variable in `Flex Pages`
+
+# v1.7.21
+## 09/14/2021
+
+1. [](#new)
+    * Added `|yaml` filter to convert input to YAML
+    * Added `route` and `request` to `onPageNotFound` event
+    * Added file upload/remove support for `Flex Forms`
+    * Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints
+    * Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()`
+    * Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages
+2. [](#improved)
+    * Use a simplified text-based `cron` field for scheduler
+    * Add timestamp to logging output of scheduler jobs to see when they ran
+3. [](#bugfix)
+    * Fixed escaping in PageIndex::getLevelListing()
+    * Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433)
+    * Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432)
+    * Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439)
+    * Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie
+
+# v1.7.20
+## 09/01/2021
+
+2. [](#improved)
+    * Added support for `task` and `action` inside JSON request body
+
+# v1.7.19
+## 08/31/2021
+
+1. [](#new)
+    * Include active form and request in `onPageTask` and `onPageAction` events (defaults to `null`)
+    * Added `UserObject::$authorizeCallable` to allow `$user->authorize()` customization
+2. [](#improved)
+    * Added meta support for `UploadedFile` class
+    * Added support for multiple mime-types per file extension [#3422](https://github.com/getgrav/grav/issues/3422)
+    * Added `setCurrent()` method to Page Collection [#3398](https://github.com/getgrav/grav/pull/3398)
+    * Initialize `$grav['uri']` before session
+3. [](#bugfix)
+    * Fixed `Warning: Undefined array key "SERVER_SOFTWARE" in index.php` [#3408](https://github.com/getgrav/grav/issues/3408)
+    * Fixed error in `loadDirectoryConfig()` if configuration hasn't been saved [#3409](https://github.com/getgrav/grav/issues/3409)
+    * Fixed GPM not using non-standard cache path [#3410](https://github.com/getgrav/grav/issues/3410)
+    * Fixed broken `environment://` stream when it doesn't have configuration
+    * Fixed `Flex Object` missing key field value when using `FolderStorage`
+    * Fixed broken Twig try tag when catch has not been defined or is empty
+    * Fixed `FlexForm` serialization
+    * Fixed form validation for numeric values in PHP 8
+    * Fixed `flex-options@` in blueprints duplicating items in array
+    * Fixed wrong form issue with flex objects after cache clear
+    * Fixed Flex object types not implementing `MediaInterface`
+    * Fixed issue with `svgImageFunction()` that was causing broken output
+
+# v1.7.18
+## 07/19/2021
+
+1. [](#improved)
+    * Added support for loading Flex Directory configuration from main configuration
+    * Move SVGs that cannot be sanitized to quarantine folder under `log://quarantine`
+    * Added support for CloudFlare-forwarded client IP in the `URI::ip()` method
+1. [](#bugfix)
+    * Fixed error when using Flex `SimpleStorage` with no entries
+    * Fixed page search to include slug field [#3316](https://github.com/getgrav/grav/issues/3316)
+    * Fixed Admin becoming unusable when GPM cannot be reached [#3383](https://github.com/getgrav/grav/issues/3383)
+    * Fixed `Failed to save entry: Forbidden` when moving a page to a visible page [#3389](https://github.com/getgrav/grav/issues/3389)
+    * Better support for Symfony local server on linux [#3400](https://github.com/getgrav/grav/pull/3400)
+    * Fixed `open_basedir()` error with some forms
+
+# v1.7.17
+## 06/15/2021
+
+1. [](#new)
+    * Interface `FlexDirectoryInterface` now extends `FlexAuthorizeInterface`
+1. [](#improved)
+    * Allow to unset an asset attribute by specifying null (ie, `'defer': null`)
+    * Support specifying custom attributes to assets in a collection [Read more](https://learn.getgrav.org/17/themes/asset-manager#collections-with-attributes?target=_blank) [#3358](https://github.com/getgrav/grav/issues/3358)
+    * File `frontmatter.yaml` isn't part of media, ignore it
+    * Switched default `JQuery` collection to use 3.x rather than 2.x
+1. [](#bugfix)
+    * Fixed missing styles when CSS/JS Pipeline is used and `asset://` folder is missing
+    * Fixed permission check when moving a page [#3382](https://github.com/getgrav/grav/issues/3382)
+
+# v1.7.16
+## 06/02/2021
+
+1. [](#new)
+    * Added 'addFrame()' method to ImageMedium [#3323](https://github.com/getgrav/grav/pull/3323)
+1. [](#improved)
+    * Set `cache.clear_images_by_default` to `false` by default
+    * Improve error on bad nested form data [#3364](https://github.com/getgrav/grav/issues/3364)
+1. [](#bugfix)
+    * Improve Plugin and Theme initialization to fix PHP8 bug [#3368](https://github.com/getgrav/grav/issues/3368)
+    * Fixed `pathinfo()` twig filter in PHP7
+    * Fixed the first visible child page getting ordering number `999999.` [#3365](https://github.com/getgrav/grav/issues/3365)
+    * Fixed flex pages search using only folder name [#3316](https://github.com/getgrav/grav/issues/3316)
+    * Fixed flex pages using wrong type in `onBlueprintCreated` event [#3157](https://github.com/getgrav/grav/issues/3157)
+    * Fixed wrong SRI paths invoked when Grav instance as a sub folder [#3358](https://github.com/getgrav/grav/issues/3358)
+    * Fixed SRI trying to calculate remote assets, only ever set integrity for local files. Use the SRI provided by the remote source and manually add it in the `addJs/addCss` call for remote support. [#3358](https://github.com/getgrav/grav/issues/3358)
+    * Fix for weird regex issue with latest PHP versions on Intel Macs causing params to not parse properly in URI object
+
+# v1.7.15
+## 05/19/2021
+
+1. [](#improved)
+    * Allow optional start date in page collections [#3350](https://github.com/getgrav/grav/pull/3350)
+    * Added `page` and `output` properties to `onOutputGenerated` and `onOutputRendered` events
+1. [](#bugfix)
+    * Fixed twig deprecated TwigFilter messages [#3348](https://github.com/getgrav/grav/issues/3348)
+    * Fixed fatal error with some markdown links [getgrav/grav-premium-issues#95](https://github.com/getgrav/grav-premium-issues/issues/95)
+    * Fixed markdown media operations not working when using `image://` stream [#3333](https://github.com/getgrav/grav/issues/3333) [#3349](https://github.com/getgrav/grav/issues/3349)
+    * Fixed copying page without changing the slug [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2139)
+    * Fixed missing and commonly used methods when using `system.twig.undefined_functions = false` [getgrav/grav-plugin-admin#2138](https://github.com/getgrav/grav-plugin-admin/issues/2138)
+    * Fixed uploading images into Flex Object if field destination is not set
+
 # v1.7.14
 ## 04/29/2021
 

+ 0 - 1
CODE_OF_CONDUCT.md

@@ -1,7 +1,6 @@
 # ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
 
 [![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
-[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
 [![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org)
  [![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors)
 

+ 6 - 6
SECURITY.md

@@ -55,12 +55,11 @@
         "miljar/php-exif": "^0.6",
         "composer/ca-bundle": "^1.2",
         "dragonmantank/cron-expression": "^1.2",
-        "phive/twig-extensions-deferred": "^1.0",
         "willdurand/negotiation": "^3.0",
         "itsgoingd/clockwork": "^5.0",
-        "enshrined/svg-sanitize": "~0.13",
         "symfony/http-client": "^4.4",
-        "composer/semver": "^1.4"
+        "composer/semver": "^1.4",
+        "rhukster/dom-sanitizer": "^1.0"
     },
     "require-dev": {
         "codeception/codeception": "^4.1",
@@ -93,7 +92,8 @@
     },
     "autoload": {
         "psr-4": {
-            "Grav\\": "system/src/Grav"
+            "Grav\\": "system/src/Grav",
+            "Twig\\": "system/src/Twig"
         },
         "files": [
             "system/defines.php"
@@ -107,8 +107,8 @@
     "scripts": {
         "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
         "post-create-project-cmd": "bin/grav install",
-        "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src",
-        "phpstan-framework": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
+        "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=520M system/src",
+        "phpstan-framework": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
         "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins",
         "test": "vendor/bin/codecept run unit",
         "test-windows": "vendor\\bin\\codecept run unit"

Файловите разлики са ограничени, защото са твърде много
+ 189 - 239
composer.lock


+ 7 - 0
fixperms.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+chown -R 1000:www-data .
+find . -type f -exec chmod 664 {} \;
+find ./bin -type f -exec chmod 775 {} \;
+find . -type d -exec chmod 775 {} \;
+find . -type d -exec chmod +s {} \;
+chmod +x ./fixperms.sh

+ 2 - 2
images/.gitkeep

@@ -17,8 +17,8 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
 }
 
 if (PHP_SAPI === 'cli-server') {
-    $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony
-') !== false;
+    $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
+
     if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
         die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
     }

+ 2 - 1
logs/.gitkeep

@@ -47,7 +47,8 @@ form:
               label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
               placeholder: '-lah'
             .at:
-              type: cron
+              type: text
+              wrapper_classes: cron-selector
               label: PLUGIN_ADMIN.SCHEDULER_RUNAT
               help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
               placeholder: '* * * * *'

+ 1 - 1
system/blueprints/config/security.yaml

@@ -646,7 +646,7 @@ form:
               type: toggle
               label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
               help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
-              highlight: 1
+              highlight: 0
               options:
                 1: PLUGIN_ADMIN.YES
                 0: PLUGIN_ADMIN.NO

+ 1 - 1
system/blueprints/flex/accounts.yaml

@@ -184,9 +184,9 @@ config:
       # Fields to be searched
       fields:
         - key
+        - slug
         - menu
         - title
-        - name
 
 blueprints:
   configure:

+ 6 - 2
system/blueprints/flex/shared/configure.yaml

@@ -28,6 +28,10 @@ types:
     type: image
     thumb: media/thumb-webp.png
     mime: image/webp
+  avif:
+    type: image
+    thumb: media/thumb.png
+    mime: image/avif
   gif:
     type: animated
     thumb: media/thumb-gif.png
@@ -91,7 +95,7 @@ types:
   aif:
     type: audio
     thumb: media/thumb-aif.png
-    mime: audio/aif
+    mime: audio/aiff
   txt:
     type: file
     thumb: media/thumb-txt.png
@@ -207,7 +211,7 @@ types:
   js:
     type: file
     thumb: media/thumb-js.png
-    mime: application/javascript
+    mime: text/javascript
   json:
     type: file
     thumb: media/thumb-json.png

+ 1986 - 0
system/config/mime.yaml

@@ -0,0 +1,1986 @@
+types:
+  '123':
+  - application/vnd.lotus-1-2-3
+  wof:
+  - application/font-woff
+  php:
+  - application/php
+  - application/x-httpd-php
+  - application/x-httpd-php-source
+  - application/x-php
+  - text/php
+  - text/x-php
+  otf:
+  - application/x-font-otf
+  - font/otf
+  ttf:
+  - application/x-font-ttf
+  - font/ttf
+  ttc:
+  - application/x-font-ttf
+  - font/collection
+  zip:
+  - application/x-gzip
+  - application/zip
+  - application/x-zip-compressed
+  amr:
+  - audio/amr
+  mp3:
+  - audio/mpeg
+  mpga:
+  - audio/mpeg
+  mp2:
+  - audio/mpeg
+  mp2a:
+  - audio/mpeg
+  m2a:
+  - audio/mpeg
+  m3a:
+  - audio/mpeg
+  jpg:
+  - image/jpeg
+  jpeg:
+  - image/jpeg
+  jpe:
+  - image/jpeg
+  bmp:
+  - image/x-ms-bmp
+  - image/bmp
+  ez:
+  - application/andrew-inset
+  aw:
+  - application/applixware
+  atom:
+  - application/atom+xml
+  atomcat:
+  - application/atomcat+xml
+  atomsvc:
+  - application/atomsvc+xml
+  ccxml:
+  - application/ccxml+xml
+  cdmia:
+  - application/cdmi-capability
+  cdmic:
+  - application/cdmi-container
+  cdmid:
+  - application/cdmi-domain
+  cdmio:
+  - application/cdmi-object
+  cdmiq:
+  - application/cdmi-queue
+  cu:
+  - application/cu-seeme
+  davmount:
+  - application/davmount+xml
+  dbk:
+  - application/docbook+xml
+  dssc:
+  - application/dssc+der
+  xdssc:
+  - application/dssc+xml
+  ecma:
+  - application/ecmascript
+  emma:
+  - application/emma+xml
+  epub:
+  - application/epub+zip
+  exi:
+  - application/exi
+  pfr:
+  - application/font-tdpfr
+  gml:
+  - application/gml+xml
+  gpx:
+  - application/gpx+xml
+  gxf:
+  - application/gxf
+  stk:
+  - application/hyperstudio
+  ink:
+  - application/inkml+xml
+  inkml:
+  - application/inkml+xml
+  ipfix:
+  - application/ipfix
+  jar:
+  - application/java-archive
+  ser:
+  - application/java-serialized-object
+  class:
+  - application/java-vm
+  js:
+  - application/javascript
+  json:
+  - application/json
+  jsonml:
+  - application/jsonml+json
+  lostxml:
+  - application/lost+xml
+  hqx:
+  - application/mac-binhex40
+  cpt:
+  - application/mac-compactpro
+  mads:
+  - application/mads+xml
+  mrc:
+  - application/marc
+  mrcx:
+  - application/marcxml+xml
+  ma:
+  - application/mathematica
+  nb:
+  - application/mathematica
+  mb:
+  - application/mathematica
+  mathml:
+  - application/mathml+xml
+  mbox:
+  - application/mbox
+  mscml:
+  - application/mediaservercontrol+xml
+  metalink:
+  - application/metalink+xml
+  meta4:
+  - application/metalink4+xml
+  mets:
+  - application/mets+xml
+  mods:
+  - application/mods+xml
+  m21:
+  - application/mp21
+  mp21:
+  - application/mp21
+  mp4s:
+  - application/mp4
+  doc:
+  - application/msword
+  dot:
+  - application/msword
+  mxf:
+  - application/mxf
+  bin:
+  - application/octet-stream
+  dms:
+  - application/octet-stream
+  lrf:
+  - application/octet-stream
+  mar:
+  - application/octet-stream
+  so:
+  - application/octet-stream
+  dist:
+  - application/octet-stream
+  distz:
+  - application/octet-stream
+  pkg:
+  - application/octet-stream
+  bpk:
+  - application/octet-stream
+  dump:
+  - application/octet-stream
+  elc:
+  - application/octet-stream
+  deploy:
+  - application/octet-stream
+  oda:
+  - application/oda
+  opf:
+  - application/oebps-package+xml
+  ogx:
+  - application/ogg
+  omdoc:
+  - application/omdoc+xml
+  onetoc:
+  - application/onenote
+  onetoc2:
+  - application/onenote
+  onetmp:
+  - application/onenote
+  onepkg:
+  - application/onenote
+  oxps:
+  - application/oxps
+  xer:
+  - application/patch-ops-error+xml
+  pdf:
+  - application/pdf
+  pgp:
+  - application/pgp-encrypted
+  asc:
+  - application/pgp-signature
+  sig:
+  - application/pgp-signature
+  prf:
+  - application/pics-rules
+  p10:
+  - application/pkcs10
+  p7m:
+  - application/pkcs7-mime
+  p7c:
+  - application/pkcs7-mime
+  p7s:
+  - application/pkcs7-signature
+  p8:
+  - application/pkcs8
+  ac:
+  - application/pkix-attr-cert
+  cer:
+  - application/pkix-cert
+  crl:
+  - application/pkix-crl
+  pkipath:
+  - application/pkix-pkipath
+  pki:
+  - application/pkixcmp
+  pls:
+  - application/pls+xml
+  ai:
+  - application/postscript
+  eps:
+  - application/postscript
+  ps:
+  - application/postscript
+  cww:
+  - application/prs.cww
+  pskcxml:
+  - application/pskc+xml
+  rdf:
+  - application/rdf+xml
+  rif:
+  - application/reginfo+xml
+  rnc:
+  - application/relax-ng-compact-syntax
+  rl:
+  - application/resource-lists+xml
+  rld:
+  - application/resource-lists-diff+xml
+  rs:
+  - application/rls-services+xml
+  gbr:
+  - application/rpki-ghostbusters
+  mft:
+  - application/rpki-manifest
+  roa:
+  - application/rpki-roa
+  rsd:
+  - application/rsd+xml
+  rss:
+  - application/rss+xml
+  rtf:
+  - application/rtf
+  sbml:
+  - application/sbml+xml
+  scq:
+  - application/scvp-cv-request
+  scs:
+  - application/scvp-cv-response
+  spq:
+  - application/scvp-vp-request
+  spp:
+  - application/scvp-vp-response
+  sdp:
+  - application/sdp
+  setpay:
+  - application/set-payment-initiation
+  setreg:
+  - application/set-registration-initiation
+  shf:
+  - application/shf+xml
+  smi:
+  - application/smil+xml
+  smil:
+  - application/smil+xml
+  rq:
+  - application/sparql-query
+  srx:
+  - application/sparql-results+xml
+  gram:
+  - application/srgs
+  grxml:
+  - application/srgs+xml
+  sru:
+  - application/sru+xml
+  ssdl:
+  - application/ssdl+xml
+  ssml:
+  - application/ssml+xml
+  tei:
+  - application/tei+xml
+  teicorpus:
+  - application/tei+xml
+  tfi:
+  - application/thraud+xml
+  tsd:
+  - application/timestamped-data
+  plb:
+  - application/vnd.3gpp.pic-bw-large
+  psb:
+  - application/vnd.3gpp.pic-bw-small
+  pvb:
+  - application/vnd.3gpp.pic-bw-var
+  tcap:
+  - application/vnd.3gpp2.tcap
+  pwn:
+  - application/vnd.3m.post-it-notes
+  aso:
+  - application/vnd.accpac.simply.aso
+  imp:
+  - application/vnd.accpac.simply.imp
+  acu:
+  - application/vnd.acucobol
+  atc:
+  - application/vnd.acucorp
+  acutc:
+  - application/vnd.acucorp
+  air:
+  - application/vnd.adobe.air-application-installer-package+zip
+  fcdt:
+  - application/vnd.adobe.formscentral.fcdt
+  fxp:
+  - application/vnd.adobe.fxp
+  fxpl:
+  - application/vnd.adobe.fxp
+  xdp:
+  - application/vnd.adobe.xdp+xml
+  xfdf:
+  - application/vnd.adobe.xfdf
+  ahead:
+  - application/vnd.ahead.space
+  azf:
+  - application/vnd.airzip.filesecure.azf
+  azs:
+  - application/vnd.airzip.filesecure.azs
+  azw:
+  - application/vnd.amazon.ebook
+  acc:
+  - application/vnd.americandynamics.acc
+  ami:
+  - application/vnd.amiga.ami
+  apk:
+  - application/vnd.android.package-archive
+  cii:
+  - application/vnd.anser-web-certificate-issue-initiation
+  fti:
+  - application/vnd.anser-web-funds-transfer-initiation
+  atx:
+  - application/vnd.antix.game-component
+  mpkg:
+  - application/vnd.apple.installer+xml
+  m3u8:
+  - application/vnd.apple.mpegurl
+  swi:
+  - application/vnd.aristanetworks.swi
+  iota:
+  - application/vnd.astraea-software.iota
+  aep:
+  - application/vnd.audiograph
+  mpm:
+  - application/vnd.blueice.multipass
+  bmi:
+  - application/vnd.bmi
+  rep:
+  - application/vnd.businessobjects
+  cdxml:
+  - application/vnd.chemdraw+xml
+  mmd:
+  - application/vnd.chipnuts.karaoke-mmd
+  cdy:
+  - application/vnd.cinderella
+  cla:
+  - application/vnd.claymore
+  rp9:
+  - application/vnd.cloanto.rp9
+  c4g:
+  - application/vnd.clonk.c4group
+  c4d:
+  - application/vnd.clonk.c4group
+  c4f:
+  - application/vnd.clonk.c4group
+  c4p:
+  - application/vnd.clonk.c4group
+  c4u:
+  - application/vnd.clonk.c4group
+  c11amc:
+  - application/vnd.cluetrust.cartomobile-config
+  c11amz:
+  - application/vnd.cluetrust.cartomobile-config-pkg
+  csp:
+  - application/vnd.commonspace
+  cdbcmsg:
+  - application/vnd.contact.cmsg
+  cmc:
+  - application/vnd.cosmocaller
+  clkx:
+  - application/vnd.crick.clicker
+  clkk:
+  - application/vnd.crick.clicker.keyboard
+  clkp:
+  - application/vnd.crick.clicker.palette
+  clkt:
+  - application/vnd.crick.clicker.template
+  clkw:
+  - application/vnd.crick.clicker.wordbank
+  wbs:
+  - application/vnd.criticaltools.wbs+xml
+  pml:
+  - application/vnd.ctc-posml
+  ppd:
+  - application/vnd.cups-ppd
+  car:
+  - application/vnd.curl.car
+  pcurl:
+  - application/vnd.curl.pcurl
+  dart:
+  - application/vnd.dart
+  rdz:
+  - application/vnd.data-vision.rdz
+  uvf:
+  - application/vnd.dece.data
+  uvvf:
+  - application/vnd.dece.data
+  uvd:
+  - application/vnd.dece.data
+  uvvd:
+  - application/vnd.dece.data
+  uvt:
+  - application/vnd.dece.ttml+xml
+  uvvt:
+  - application/vnd.dece.ttml+xml
+  uvx:
+  - application/vnd.dece.unspecified
+  uvvx:
+  - application/vnd.dece.unspecified
+  uvz:
+  - application/vnd.dece.zip
+  uvvz:
+  - application/vnd.dece.zip
+  fe_launch:
+  - application/vnd.denovo.fcselayout-link
+  dna:
+  - application/vnd.dna
+  mlp:
+  - application/vnd.dolby.mlp
+  dpg:
+  - application/vnd.dpgraph
+  dfac:
+  - application/vnd.dreamfactory
+  kpxx:
+  - application/vnd.ds-keypoint
+  ait:
+  - application/vnd.dvb.ait
+  svc:
+  - application/vnd.dvb.service
+  geo:
+  - application/vnd.dynageo
+  mag:
+  - application/vnd.ecowin.chart
+  nml:
+  - application/vnd.enliven
+  esf:
+  - application/vnd.epson.esf
+  msf:
+  - application/vnd.epson.msf
+  qam:
+  - application/vnd.epson.quickanime
+  slt:
+  - application/vnd.epson.salt
+  ssf:
+  - application/vnd.epson.ssf
+  es3:
+  - application/vnd.eszigno3+xml
+  et3:
+  - application/vnd.eszigno3+xml
+  ez2:
+  - application/vnd.ezpix-album
+  ez3:
+  - application/vnd.ezpix-package
+  fdf:
+  - application/vnd.fdf
+  mseed:
+  - application/vnd.fdsn.mseed
+  seed:
+  - application/vnd.fdsn.seed
+  dataless:
+  - application/vnd.fdsn.seed
+  gph:
+  - application/vnd.flographit
+  ftc:
+  - application/vnd.fluxtime.clip
+  fm:
+  - application/vnd.framemaker
+  frame:
+  - application/vnd.framemaker
+  maker:
+  - application/vnd.framemaker
+  book:
+  - application/vnd.framemaker
+  fnc:
+  - application/vnd.frogans.fnc
+  ltf:
+  - application/vnd.frogans.ltf
+  fsc:
+  - application/vnd.fsc.weblaunch
+  oas:
+  - application/vnd.fujitsu.oasys
+  oa2:
+  - application/vnd.fujitsu.oasys2
+  oa3:
+  - application/vnd.fujitsu.oasys3
+  fg5:
+  - application/vnd.fujitsu.oasysgp
+  bh2:
+  - application/vnd.fujitsu.oasysprs
+  ddd:
+  - application/vnd.fujixerox.ddd
+  xdw:
+  - application/vnd.fujixerox.docuworks
+  xbd:
+  - application/vnd.fujixerox.docuworks.binder
+  fzs:
+  - application/vnd.fuzzysheet
+  txd:
+  - application/vnd.genomatix.tuxedo
+  ggb:
+  - application/vnd.geogebra.file
+  ggt:
+  - application/vnd.geogebra.tool
+  gex:
+  - application/vnd.geometry-explorer
+  gre:
+  - application/vnd.geometry-explorer
+  gxt:
+  - application/vnd.geonext
+  g2w:
+  - application/vnd.geoplan
+  g3w:
+  - application/vnd.geospace
+  gmx:
+  - application/vnd.gmx
+  kml:
+  - application/vnd.google-earth.kml+xml
+  kmz:
+  - application/vnd.google-earth.kmz
+  gqf:
+  - application/vnd.grafeq
+  gqs:
+  - application/vnd.grafeq
+  gac:
+  - application/vnd.groove-account
+  ghf:
+  - application/vnd.groove-help
+  gim:
+  - application/vnd.groove-identity-message
+  grv:
+  - application/vnd.groove-injector
+  gtm:
+  - application/vnd.groove-tool-message
+  tpl:
+  - application/vnd.groove-tool-template
+  vcg:
+  - application/vnd.groove-vcard
+  hal:
+  - application/vnd.hal+xml
+  zmm:
+  - application/vnd.handheld-entertainment+xml
+  hbci:
+  - application/vnd.hbci
+  les:
+  - application/vnd.hhe.lesson-player
+  hpgl:
+  - application/vnd.hp-hpgl
+  hpid:
+  - application/vnd.hp-hpid
+  hps:
+  - application/vnd.hp-hps
+  jlt:
+  - application/vnd.hp-jlyt
+  pcl:
+  - application/vnd.hp-pcl
+  pclxl:
+  - application/vnd.hp-pclxl
+  sfd-hdstx:
+  - application/vnd.hydrostatix.sof-data
+  mpy:
+  - application/vnd.ibm.minipay
+  afp:
+  - application/vnd.ibm.modcap
+  listafp:
+  - application/vnd.ibm.modcap
+  list3820:
+  - application/vnd.ibm.modcap
+  irm:
+  - application/vnd.ibm.rights-management
+  sc:
+  - application/vnd.ibm.secure-container
+  icc:
+  - application/vnd.iccprofile
+  icm:
+  - application/vnd.iccprofile
+  igl:
+  - application/vnd.igloader
+  ivp:
+  - application/vnd.immervision-ivp
+  ivu:
+  - application/vnd.immervision-ivu
+  igm:
+  - application/vnd.insors.igm
+  xpw:
+  - application/vnd.intercon.formnet
+  xpx:
+  - application/vnd.intercon.formnet
+  i2g:
+  - application/vnd.intergeo
+  qbo:
+  - application/vnd.intu.qbo
+  qfx:
+  - application/vnd.intu.qfx
+  rcprofile:
+  - application/vnd.ipunplugged.rcprofile
+  irp:
+  - application/vnd.irepository.package+xml
+  xpr:
+  - application/vnd.is-xpr
+  fcs:
+  - application/vnd.isac.fcs
+  jam:
+  - application/vnd.jam
+  rms:
+  - application/vnd.jcp.javame.midlet-rms
+  jisp:
+  - application/vnd.jisp
+  joda:
+  - application/vnd.joost.joda-archive
+  ktz:
+  - application/vnd.kahootz
+  ktr:
+  - application/vnd.kahootz
+  karbon:
+  - application/vnd.kde.karbon
+  chrt:
+  - application/vnd.kde.kchart
+  kfo:
+  - application/vnd.kde.kformula
+  flw:
+  - application/vnd.kde.kivio
+  kon:
+  - application/vnd.kde.kontour
+  kpr:
+  - application/vnd.kde.kpresenter
+  kpt:
+  - application/vnd.kde.kpresenter
+  ksp:
+  - application/vnd.kde.kspread
+  kwd:
+  - application/vnd.kde.kword
+  kwt:
+  - application/vnd.kde.kword
+  htke:
+  - application/vnd.kenameaapp
+  kia:
+  - application/vnd.kidspiration
+  kne:
+  - application/vnd.kinar
+  knp:
+  - application/vnd.kinar
+  skp:
+  - application/vnd.koan
+  skd:
+  - application/vnd.koan
+  skt:
+  - application/vnd.koan
+  skm:
+  - application/vnd.koan
+  sse:
+  - application/vnd.kodak-descriptor
+  lasxml:
+  - application/vnd.las.las+xml
+  lbd:
+  - application/vnd.llamagraphics.life-balance.desktop
+  lbe:
+  - application/vnd.llamagraphics.life-balance.exchange+xml
+  apr:
+  - application/vnd.lotus-approach
+  pre:
+  - application/vnd.lotus-freelance
+  nsf:
+  - application/vnd.lotus-notes
+  org:
+  - application/vnd.lotus-organizer
+  scm:
+  - application/vnd.lotus-screencam
+  lwp:
+  - application/vnd.lotus-wordpro
+  portpkg:
+  - application/vnd.macports.portpkg
+  mcd:
+  - application/vnd.mcd
+  mc1:
+  - application/vnd.medcalcdata
+  cdkey:
+  - application/vnd.mediastation.cdkey
+  mwf:
+  - application/vnd.mfer
+  mfm:
+  - application/vnd.mfmp
+  flo:
+  - application/vnd.micrografx.flo
+  igx:
+  - application/vnd.micrografx.igx
+  mif:
+  - application/vnd.mif
+  daf:
+  - application/vnd.mobius.daf
+  dis:
+  - application/vnd.mobius.dis
+  mbk:
+  - application/vnd.mobius.mbk
+  mqy:
+  - application/vnd.mobius.mqy
+  msl:
+  - application/vnd.mobius.msl
+  plc:
+  - application/vnd.mobius.plc
+  txf:
+  - application/vnd.mobius.txf
+  mpn:
+  - application/vnd.mophun.application
+  mpc:
+  - application/vnd.mophun.certificate
+  xul:
+  - application/vnd.mozilla.xul+xml
+  cil:
+  - application/vnd.ms-artgalry
+  cab:
+  - application/vnd.ms-cab-compressed
+  xls:
+  - application/vnd.ms-excel
+  xlm:
+  - application/vnd.ms-excel
+  xla:
+  - application/vnd.ms-excel
+  xlc:
+  - application/vnd.ms-excel
+  xlt:
+  - application/vnd.ms-excel
+  xlw:
+  - application/vnd.ms-excel
+  xlam:
+  - application/vnd.ms-excel.addin.macroenabled.12
+  xlsb:
+  - application/vnd.ms-excel.sheet.binary.macroenabled.12
+  xlsm:
+  - application/vnd.ms-excel.sheet.macroenabled.12
+  xltm:
+  - application/vnd.ms-excel.template.macroenabled.12
+  eot:
+  - application/vnd.ms-fontobject
+  chm:
+  - application/vnd.ms-htmlhelp
+  ims:
+  - application/vnd.ms-ims
+  lrm:
+  - application/vnd.ms-lrm
+  thmx:
+  - application/vnd.ms-officetheme
+  cat:
+  - application/vnd.ms-pki.seccat
+  stl:
+  - application/vnd.ms-pki.stl
+  ppt:
+  - application/vnd.ms-powerpoint
+  pps:
+  - application/vnd.ms-powerpoint
+  pot:
+  - application/vnd.ms-powerpoint
+  ppam:
+  - application/vnd.ms-powerpoint.addin.macroenabled.12
+  pptm:
+  - application/vnd.ms-powerpoint.presentation.macroenabled.12
+  sldm:
+  - application/vnd.ms-powerpoint.slide.macroenabled.12
+  ppsm:
+  - application/vnd.ms-powerpoint.slideshow.macroenabled.12
+  potm:
+  - application/vnd.ms-powerpoint.template.macroenabled.12
+  mpp:
+  - application/vnd.ms-project
+  mpt:
+  - application/vnd.ms-project
+  docm:
+  - application/vnd.ms-word.document.macroenabled.12
+  dotm:
+  - application/vnd.ms-word.template.macroenabled.12
+  wps:
+  - application/vnd.ms-works
+  wks:
+  - application/vnd.ms-works
+  wcm:
+  - application/vnd.ms-works
+  wdb:
+  - application/vnd.ms-works
+  wpl:
+  - application/vnd.ms-wpl
+  xps:
+  - application/vnd.ms-xpsdocument
+  mseq:
+  - application/vnd.mseq
+  mus:
+  - application/vnd.musician
+  msty:
+  - application/vnd.muvee.style
+  taglet:
+  - application/vnd.mynfc
+  nlu:
+  - application/vnd.neurolanguage.nlu
+  ntf:
+  - application/vnd.nitf
+  nitf:
+  - application/vnd.nitf
+  nnd:
+  - application/vnd.noblenet-directory
+  nns:
+  - application/vnd.noblenet-sealer
+  nnw:
+  - application/vnd.noblenet-web
+  ngdat:
+  - application/vnd.nokia.n-gage.data
+  n-gage:
+  - application/vnd.nokia.n-gage.symbian.install
+  rpst:
+  - application/vnd.nokia.radio-preset
+  rpss:
+  - application/vnd.nokia.radio-presets
+  edm:
+  - application/vnd.novadigm.edm
+  edx:
+  - application/vnd.novadigm.edx
+  ext:
+  - application/vnd.novadigm.ext
+  odc:
+  - application/vnd.oasis.opendocument.chart
+  otc:
+  - application/vnd.oasis.opendocument.chart-template
+  odb:
+  - application/vnd.oasis.opendocument.database
+  odf:
+  - application/vnd.oasis.opendocument.formula
+  odft:
+  - application/vnd.oasis.opendocument.formula-template
+  odg:
+  - application/vnd.oasis.opendocument.graphics
+  otg:
+  - application/vnd.oasis.opendocument.graphics-template
+  odi:
+  - application/vnd.oasis.opendocument.image
+  oti:
+  - application/vnd.oasis.opendocument.image-template
+  odp:
+  - application/vnd.oasis.opendocument.presentation
+  otp:
+  - application/vnd.oasis.opendocument.presentation-template
+  ods:
+  - application/vnd.oasis.opendocument.spreadsheet
+  ots:
+  - application/vnd.oasis.opendocument.spreadsheet-template
+  odt:
+  - application/vnd.oasis.opendocument.text
+  odm:
+  - application/vnd.oasis.opendocument.text-master
+  ott:
+  - application/vnd.oasis.opendocument.text-template
+  oth:
+  - application/vnd.oasis.opendocument.text-web
+  xo:
+  - application/vnd.olpc-sugar
+  dd2:
+  - application/vnd.oma.dd2+xml
+  oxt:
+  - application/vnd.openofficeorg.extension
+  pptx:
+  - application/vnd.openxmlformats-officedocument.presentationml.presentation
+  sldx:
+  - application/vnd.openxmlformats-officedocument.presentationml.slide
+  ppsx:
+  - application/vnd.openxmlformats-officedocument.presentationml.slideshow
+  potx:
+  - application/vnd.openxmlformats-officedocument.presentationml.template
+  xlsx:
+  - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+  xltx:
+  - application/vnd.openxmlformats-officedocument.spreadsheetml.template
+  docx:
+  - application/vnd.openxmlformats-officedocument.wordprocessingml.document
+  dotx:
+  - application/vnd.openxmlformats-officedocument.wordprocessingml.template
+  mgp:
+  - application/vnd.osgeo.mapguide.package
+  dp:
+  - application/vnd.osgi.dp
+  esa:
+  - application/vnd.osgi.subsystem
+  pdb:
+  - application/vnd.palm
+  pqa:
+  - application/vnd.palm
+  oprc:
+  - application/vnd.palm
+  paw:
+  - application/vnd.pawaafile
+  str:
+  - application/vnd.pg.format
+  ei6:
+  - application/vnd.pg.osasli
+  efif:
+  - application/vnd.picsel
+  wg:
+  - application/vnd.pmi.widget
+  plf:
+  - application/vnd.pocketlearn
+  pbd:
+  - application/vnd.powerbuilder6
+  box:
+  - application/vnd.previewsystems.box
+  mgz:
+  - application/vnd.proteus.magazine
+  qps:
+  - application/vnd.publishare-delta-tree
+  ptid:
+  - application/vnd.pvi.ptid1
+  qxd:
+  - application/vnd.quark.quarkxpress
+  qxt:
+  - application/vnd.quark.quarkxpress
+  qwd:
+  - application/vnd.quark.quarkxpress
+  qwt:
+  - application/vnd.quark.quarkxpress
+  qxl:
+  - application/vnd.quark.quarkxpress
+  qxb:
+  - application/vnd.quark.quarkxpress
+  bed:
+  - application/vnd.realvnc.bed
+  mxl:
+  - application/vnd.recordare.musicxml
+  musicxml:
+  - application/vnd.recordare.musicxml+xml
+  cryptonote:
+  - application/vnd.rig.cryptonote
+  cod:
+  - application/vnd.rim.cod
+  rm:
+  - application/vnd.rn-realmedia
+  rmvb:
+  - application/vnd.rn-realmedia-vbr
+  link66:
+  - application/vnd.route66.link66+xml
+  st:
+  - application/vnd.sailingtracker.track
+  see:
+  - application/vnd.seemail
+  sema:
+  - application/vnd.sema
+  semd:
+  - application/vnd.semd
+  semf:
+  - application/vnd.semf
+  ifm:
+  - application/vnd.shana.informed.formdata
+  itp:
+  - application/vnd.shana.informed.formtemplate
+  iif:
+  - application/vnd.shana.informed.interchange
+  ipk:
+  - application/vnd.shana.informed.package
+  twd:
+  - application/vnd.simtech-mindmapper
+  twds:
+  - application/vnd.simtech-mindmapper
+  mmf:
+  - application/vnd.smaf
+  teacher:
+  - application/vnd.smart.teacher
+  sdkm:
+  - application/vnd.solent.sdkm+xml
+  sdkd:
+  - application/vnd.solent.sdkm+xml
+  dxp:
+  - application/vnd.spotfire.dxp
+  sfs:
+  - application/vnd.spotfire.sfs
+  sdc:
+  - application/vnd.stardivision.calc
+  sda:
+  - application/vnd.stardivision.draw
+  sdd:
+  - application/vnd.stardivision.impress
+  smf:
+  - application/vnd.stardivision.math
+  sdw:
+  - application/vnd.stardivision.writer
+  vor:
+  - application/vnd.stardivision.writer
+  sgl:
+  - application/vnd.stardivision.writer-global
+  smzip:
+  - application/vnd.stepmania.package
+  sm:
+  - application/vnd.stepmania.stepchart
+  sxc:
+  - application/vnd.sun.xml.calc
+  stc:
+  - application/vnd.sun.xml.calc.template
+  sxd:
+  - application/vnd.sun.xml.draw
+  std:
+  - application/vnd.sun.xml.draw.template
+  sxi:
+  - application/vnd.sun.xml.impress
+  sti:
+  - application/vnd.sun.xml.impress.template
+  sxm:
+  - application/vnd.sun.xml.math
+  sxw:
+  - application/vnd.sun.xml.writer
+  sxg:
+  - application/vnd.sun.xml.writer.global
+  stw:
+  - application/vnd.sun.xml.writer.template
+  sus:
+  - application/vnd.sus-calendar
+  susp:
+  - application/vnd.sus-calendar
+  svd:
+  - application/vnd.svd
+  sis:
+  - application/vnd.symbian.install
+  sisx:
+  - application/vnd.symbian.install
+  xsm:
+  - application/vnd.syncml+xml
+  bdm:
+  - application/vnd.syncml.dm+wbxml
+  xdm:
+  - application/vnd.syncml.dm+xml
+  tao:
+  - application/vnd.tao.intent-module-archive
+  pcap:
+  - application/vnd.tcpdump.pcap
+  cap:
+  - application/vnd.tcpdump.pcap
+  dmp:
+  - application/vnd.tcpdump.pcap
+  tmo:
+  - application/vnd.tmobile-livetv
+  tpt:
+  - application/vnd.trid.tpt
+  mxs:
+  - application/vnd.triscape.mxs
+  tra:
+  - application/vnd.trueapp
+  ufd:
+  - application/vnd.ufdl
+  ufdl:
+  - application/vnd.ufdl
+  utz:
+  - application/vnd.uiq.theme
+  umj:
+  - application/vnd.umajin
+  unityweb:
+  - application/vnd.unity
+  uoml:
+  - application/vnd.uoml+xml
+  vcx:
+  - application/vnd.vcx
+  vsd:
+  - application/vnd.visio
+  vst:
+  - application/vnd.visio
+  vss:
+  - application/vnd.visio
+  vsw:
+  - application/vnd.visio
+  vis:
+  - application/vnd.visionary
+  vsf:
+  - application/vnd.vsf
+  wbxml:
+  - application/vnd.wap.wbxml
+  wmlc:
+  - application/vnd.wap.wmlc
+  wmlsc:
+  - application/vnd.wap.wmlscriptc
+  wtb:
+  - application/vnd.webturbo
+  nbp:
+  - application/vnd.wolfram.player
+  wpd:
+  - application/vnd.wordperfect
+  wqd:
+  - application/vnd.wqd
+  stf:
+  - application/vnd.wt.stf
+  xar:
+  - application/vnd.xara
+  xfdl:
+  - application/vnd.xfdl
+  hvd:
+  - application/vnd.yamaha.hv-dic
+  hvs:
+  - application/vnd.yamaha.hv-script
+  hvp:
+  - application/vnd.yamaha.hv-voice
+  osf:
+  - application/vnd.yamaha.openscoreformat
+  osfpvg:
+  - application/vnd.yamaha.openscoreformat.osfpvg+xml
+  saf:
+  - application/vnd.yamaha.smaf-audio
+  spf:
+  - application/vnd.yamaha.smaf-phrase
+  cmp:
+  - application/vnd.yellowriver-custom-menu
+  zir:
+  - application/vnd.zul
+  zirz:
+  - application/vnd.zul
+  zaz:
+  - application/vnd.zzazz.deck+xml
+  vxml:
+  - application/voicexml+xml
+  wgt:
+  - application/widget
+  hlp:
+  - application/winhlp
+  wsdl:
+  - application/wsdl+xml
+  wspolicy:
+  - application/wspolicy+xml
+  7z:
+  - application/x-7z-compressed
+  abw:
+  - application/x-abiword
+  ace:
+  - application/x-ace-compressed
+  dmg:
+  - application/x-apple-diskimage
+  aab:
+  - application/x-authorware-bin
+  x32:
+  - application/x-authorware-bin
+  u32:
+  - application/x-authorware-bin
+  vox:
+  - application/x-authorware-bin
+  aam:
+  - application/x-authorware-map
+  aas:
+  - application/x-authorware-seg
+  bcpio:
+  - application/x-bcpio
+  torrent:
+  - application/x-bittorrent
+  blb:
+  - application/x-blorb
+  blorb:
+  - application/x-blorb
+  bz:
+  - application/x-bzip
+  bz2:
+  - application/x-bzip2
+  boz:
+  - application/x-bzip2
+  cbr:
+  - application/x-cbr
+  cba:
+  - application/x-cbr
+  cbt:
+  - application/x-cbr
+  cbz:
+  - application/x-cbr
+  cb7:
+  - application/x-cbr
+  vcd:
+  - application/x-cdlink
+  cfs:
+  - application/x-cfs-compressed
+  chat:
+  - application/x-chat
+  pgn:
+  - application/x-chess-pgn
+  nsc:
+  - application/x-conference
+  cpio:
+  - application/x-cpio
+  csh:
+  - application/x-csh
+  deb:
+  - application/x-debian-package
+  udeb:
+  - application/x-debian-package
+  dgc:
+  - application/x-dgc-compressed
+  dir:
+  - application/x-director
+  dcr:
+  - application/x-director
+  dxr:
+  - application/x-director
+  cst:
+  - application/x-director
+  cct:
+  - application/x-director
+  cxt:
+  - application/x-director
+  w3d:
+  - application/x-director
+  fgd:
+  - application/x-director
+  swa:
+  - application/x-director
+  wad:
+  - application/x-doom
+  ncx:
+  - application/x-dtbncx+xml
+  dtb:
+  - application/x-dtbook+xml
+  res:
+  - application/x-dtbresource+xml
+  dvi:
+  - application/x-dvi
+  evy:
+  - application/x-envoy
+  eva:
+  - application/x-eva
+  bdf:
+  - application/x-font-bdf
+  gsf:
+  - application/x-font-ghostscript
+  psf:
+  - application/x-font-linux-psf
+  pcf:
+  - application/x-font-pcf
+  snf:
+  - application/x-font-snf
+  pfa:
+  - application/x-font-type1
+  pfb:
+  - application/x-font-type1
+  pfm:
+  - application/x-font-type1
+  afm:
+  - application/x-font-type1
+  arc:
+  - application/x-freearc
+  spl:
+  - application/x-futuresplash
+  gca:
+  - application/x-gca-compressed
+  ulx:
+  - application/x-glulx
+  gnumeric:
+  - application/x-gnumeric
+  gramps:
+  - application/x-gramps-xml
+  gtar:
+  - application/x-gtar
+  hdf:
+  - application/x-hdf
+  install:
+  - application/x-install-instructions
+  iso:
+  - application/x-iso9660-image
+  jnlp:
+  - application/x-java-jnlp-file
+  latex:
+  - application/x-latex
+  lzh:
+  - application/x-lzh-compressed
+  lha:
+  - application/x-lzh-compressed
+  mie:
+  - application/x-mie
+  prc:
+  - application/x-mobipocket-ebook
+  mobi:
+  - application/x-mobipocket-ebook
+  application:
+  - application/x-ms-application
+  lnk:
+  - application/x-ms-shortcut
+  wmd:
+  - application/x-ms-wmd
+  wmz:
+  - application/x-ms-wmz
+  - application/x-msmetafile
+  xbap:
+  - application/x-ms-xbap
+  mdb:
+  - application/x-msaccess
+  obd:
+  - application/x-msbinder
+  crd:
+  - application/x-mscardfile
+  clp:
+  - application/x-msclip
+  exe:
+  - application/x-msdownload
+  dll:
+  - application/x-msdownload
+  com:
+  - application/x-msdownload
+  bat:
+  - application/x-msdownload
+  msi:
+  - application/x-msdownload
+  mvb:
+  - application/x-msmediaview
+  m13:
+  - application/x-msmediaview
+  m14:
+  - application/x-msmediaview
+  wmf:
+  - application/x-msmetafile
+  emf:
+  - application/x-msmetafile
+  emz:
+  - application/x-msmetafile
+  mny:
+  - application/x-msmoney
+  pub:
+  - application/x-mspublisher
+  scd:
+  - application/x-msschedule
+  trm:
+  - application/x-msterminal
+  wri:
+  - application/x-mswrite
+  nc:
+  - application/x-netcdf
+  cdf:
+  - application/x-netcdf
+  nzb:
+  - application/x-nzb
+  p12:
+  - application/x-pkcs12
+  pfx:
+  - application/x-pkcs12
+  p7b:
+  - application/x-pkcs7-certificates
+  spc:
+  - application/x-pkcs7-certificates
+  p7r:
+  - application/x-pkcs7-certreqresp
+  rar:
+  - application/x-rar-compressed
+  ris:
+  - application/x-research-info-systems
+  sh:
+  - application/x-sh
+  shar:
+  - application/x-shar
+  swf:
+  - application/x-shockwave-flash
+  xap:
+  - application/x-silverlight-app
+  sql:
+  - application/x-sql
+  sit:
+  - application/x-stuffit
+  sitx:
+  - application/x-stuffitx
+  srt:
+  - application/x-subrip
+  sv4cpio:
+  - application/x-sv4cpio
+  sv4crc:
+  - application/x-sv4crc
+  t3:
+  - application/x-t3vm-image
+  gam:
+  - application/x-tads
+  tar:
+  - application/x-tar
+  tcl:
+  - application/x-tcl
+  tex:
+  - application/x-tex
+  tfm:
+  - application/x-tex-tfm
+  texinfo:
+  - application/x-texinfo
+  texi:
+  - application/x-texinfo
+  obj:
+  - application/x-tgif
+  ustar:
+  - application/x-ustar
+  src:
+  - application/x-wais-source
+  der:
+  - application/x-x509-ca-cert
+  crt:
+  - application/x-x509-ca-cert
+  fig:
+  - application/x-xfig
+  xlf:
+  - application/x-xliff+xml
+  xpi:
+  - application/x-xpinstall
+  xz:
+  - application/x-xz
+  z1:
+  - application/x-zmachine
+  z2:
+  - application/x-zmachine
+  z3:
+  - application/x-zmachine
+  z4:
+  - application/x-zmachine
+  z5:
+  - application/x-zmachine
+  z6:
+  - application/x-zmachine
+  z7:
+  - application/x-zmachine
+  z8:
+  - application/x-zmachine
+  xaml:
+  - application/xaml+xml
+  xdf:
+  - application/xcap-diff+xml
+  xenc:
+  - application/xenc+xml
+  xhtml:
+  - application/xhtml+xml
+  xht:
+  - application/xhtml+xml
+  xml:
+  - application/xml
+  xsl:
+  - application/xml
+  dtd:
+  - application/xml-dtd
+  xop:
+  - application/xop+xml
+  xpl:
+  - application/xproc+xml
+  xslt:
+  - application/xslt+xml
+  xspf:
+  - application/xspf+xml
+  mxml:
+  - application/xv+xml
+  xhvml:
+  - application/xv+xml
+  xvml:
+  - application/xv+xml
+  xvm:
+  - application/xv+xml
+  yang:
+  - application/yang
+  yin:
+  - application/yin+xml
+  adp:
+  - audio/adpcm
+  au:
+  - audio/basic
+  snd:
+  - audio/basic
+  mid:
+  - audio/midi
+  midi:
+  - audio/midi
+  kar:
+  - audio/midi
+  rmi:
+  - audio/midi
+  m4a:
+  - audio/mp4
+  mp4a:
+  - audio/mp4
+  oga:
+  - audio/ogg
+  ogg:
+  - audio/ogg
+  spx:
+  - audio/ogg
+  s3m:
+  - audio/s3m
+  sil:
+  - audio/silk
+  uva:
+  - audio/vnd.dece.audio
+  uvva:
+  - audio/vnd.dece.audio
+  eol:
+  - audio/vnd.digital-winds
+  dra:
+  - audio/vnd.dra
+  dts:
+  - audio/vnd.dts
+  dtshd:
+  - audio/vnd.dts.hd
+  lvp:
+  - audio/vnd.lucent.voice
+  pya:
+  - audio/vnd.ms-playready.media.pya
+  ecelp4800:
+  - audio/vnd.nuera.ecelp4800
+  ecelp7470:
+  - audio/vnd.nuera.ecelp7470
+  ecelp9600:
+  - audio/vnd.nuera.ecelp9600
+  rip:
+  - audio/vnd.rip
+  weba:
+  - audio/webm
+  aac:
+  - audio/x-aac
+  aif:
+  - audio/x-aiff
+  aiff:
+  - audio/x-aiff
+  aifc:
+  - audio/x-aiff
+  caf:
+  - audio/x-caf
+  flac:
+  - audio/x-flac
+  mka:
+  - audio/x-matroska
+  m3u:
+  - audio/x-mpegurl
+  wax:
+  - audio/x-ms-wax
+  wma:
+  - audio/x-ms-wma
+  ram:
+  - audio/x-pn-realaudio
+  ra:
+  - audio/x-pn-realaudio
+  rmp:
+  - audio/x-pn-realaudio-plugin
+  wav:
+  - audio/x-wav
+  xm:
+  - audio/xm
+  cdx:
+  - chemical/x-cdx
+  cif:
+  - chemical/x-cif
+  cmdf:
+  - chemical/x-cmdf
+  cml:
+  - chemical/x-cml
+  csml:
+  - chemical/x-csml
+  xyz:
+  - chemical/x-xyz
+  woff:
+  - font/woff
+  woff2:
+  - font/woff2
+  cgm:
+  - image/cgm
+  g3:
+  - image/g3fax
+  gif:
+  - image/gif
+  ief:
+  - image/ief
+  ktx:
+  - image/ktx
+  png:
+  - image/png
+  btif:
+  - image/prs.btif
+  sgi:
+  - image/sgi
+  svg:
+  - image/svg+xml
+  svgz:
+  - image/svg+xml
+  tiff:
+  - image/tiff
+  tif:
+  - image/tiff
+  psd:
+  - image/vnd.adobe.photoshop
+  uvi:
+  - image/vnd.dece.graphic
+  uvvi:
+  - image/vnd.dece.graphic
+  uvg:
+  - image/vnd.dece.graphic
+  uvvg:
+  - image/vnd.dece.graphic
+  djvu:
+  - image/vnd.djvu
+  djv:
+  - image/vnd.djvu
+  sub:
+  - image/vnd.dvb.subtitle
+  - text/vnd.dvb.subtitle
+  dwg:
+  - image/vnd.dwg
+  dxf:
+  - image/vnd.dxf
+  fbs:
+  - image/vnd.fastbidsheet
+  fpx:
+  - image/vnd.fpx
+  fst:
+  - image/vnd.fst
+  mmr:
+  - image/vnd.fujixerox.edmics-mmr
+  rlc:
+  - image/vnd.fujixerox.edmics-rlc
+  mdi:
+  - image/vnd.ms-modi
+  wdp:
+  - image/vnd.ms-photo
+  npx:
+  - image/vnd.net-fpx
+  wbmp:
+  - image/vnd.wap.wbmp
+  xif:
+  - image/vnd.xiff
+  webp:
+  - image/webp
+  3ds:
+  - image/x-3ds
+  ras:
+  - image/x-cmu-raster
+  cmx:
+  - image/x-cmx
+  fh:
+  - image/x-freehand
+  fhc:
+  - image/x-freehand
+  fh4:
+  - image/x-freehand
+  fh5:
+  - image/x-freehand
+  fh7:
+  - image/x-freehand
+  ico:
+  - image/x-icon
+  sid:
+  - image/x-mrsid-image
+  pcx:
+  - image/x-pcx
+  pic:
+  - image/x-pict
+  pct:
+  - image/x-pict
+  pnm:
+  - image/x-portable-anymap
+  pbm:
+  - image/x-portable-bitmap
+  pgm:
+  - image/x-portable-graymap
+  ppm:
+  - image/x-portable-pixmap
+  rgb:
+  - image/x-rgb
+  tga:
+  - image/x-tga
+  xbm:
+  - image/x-xbitmap
+  xpm:
+  - image/x-xpixmap
+  xwd:
+  - image/x-xwindowdump
+  eml:
+  - message/rfc822
+  mime:
+  - message/rfc822
+  igs:
+  - model/iges
+  iges:
+  - model/iges
+  msh:
+  - model/mesh
+  mesh:
+  - model/mesh
+  silo:
+  - model/mesh
+  dae:
+  - model/vnd.collada+xml
+  dwf:
+  - model/vnd.dwf
+  gdl:
+  - model/vnd.gdl
+  gtw:
+  - model/vnd.gtw
+  mts:
+  - model/vnd.mts
+  vtu:
+  - model/vnd.vtu
+  wrl:
+  - model/vrml
+  vrml:
+  - model/vrml
+  x3db:
+  - model/x3d+binary
+  x3dbz:
+  - model/x3d+binary
+  x3dv:
+  - model/x3d+vrml
+  x3dvz:
+  - model/x3d+vrml
+  x3d:
+  - model/x3d+xml
+  x3dz:
+  - model/x3d+xml
+  appcache:
+  - text/cache-manifest
+  ics:
+  - text/calendar
+  ifb:
+  - text/calendar
+  css:
+  - text/css
+  csv:
+  - text/csv
+  html:
+  - text/html
+  htm:
+  - text/html
+  n3:
+  - text/n3
+  txt:
+  - text/plain
+  text:
+  - text/plain
+  conf:
+  - text/plain
+  def:
+  - text/plain
+  list:
+  - text/plain
+  log:
+  - text/plain
+  in:
+  - text/plain
+  dsc:
+  - text/prs.lines.tag
+  rtx:
+  - text/richtext
+  sgml:
+  - text/sgml
+  sgm:
+  - text/sgml
+  tsv:
+  - text/tab-separated-values
+  t:
+  - text/troff
+  tr:
+  - text/troff
+  roff:
+  - text/troff
+  man:
+  - text/troff
+  me:
+  - text/troff
+  ms:
+  - text/troff
+  ttl:
+  - text/turtle
+  uri:
+  - text/uri-list
+  uris:
+  - text/uri-list
+  urls:
+  - text/uri-list
+  vcard:
+  - text/vcard
+  curl:
+  - text/vnd.curl
+  dcurl:
+  - text/vnd.curl.dcurl
+  mcurl:
+  - text/vnd.curl.mcurl
+  scurl:
+  - text/vnd.curl.scurl
+  fly:
+  - text/vnd.fly
+  flx:
+  - text/vnd.fmi.flexstor
+  gv:
+  - text/vnd.graphviz
+  3dml:
+  - text/vnd.in3d.3dml
+  spot:
+  - text/vnd.in3d.spot
+  jad:
+  - text/vnd.sun.j2me.app-descriptor
+  wml:
+  - text/vnd.wap.wml
+  wmls:
+  - text/vnd.wap.wmlscript
+  s:
+  - text/x-asm
+  asm:
+  - text/x-asm
+  c:
+  - text/x-c
+  cc:
+  - text/x-c
+  cxx:
+  - text/x-c
+  cpp:
+  - text/x-c
+  h:
+  - text/x-c
+  hh:
+  - text/x-c
+  dic:
+  - text/x-c
+  f:
+  - text/x-fortran
+  for:
+  - text/x-fortran
+  f77:
+  - text/x-fortran
+  f90:
+  - text/x-fortran
+  java:
+  - text/x-java-source
+  nfo:
+  - text/x-nfo
+  opml:
+  - text/x-opml
+  p:
+  - text/x-pascal
+  pas:
+  - text/x-pascal
+  etx:
+  - text/x-setext
+  sfv:
+  - text/x-sfv
+  uu:
+  - text/x-uuencode
+  vcs:
+  - text/x-vcalendar
+  vcf:
+  - text/x-vcard
+  3gp:
+  - video/3gpp
+  3g2:
+  - video/3gpp2
+  h261:
+  - video/h261
+  h263:
+  - video/h263
+  h264:
+  - video/h264
+  jpgv:
+  - video/jpeg
+  jpm:
+  - video/jpm
+  jpgm:
+  - video/jpm
+  mj2:
+  - video/mj2
+  mjp2:
+  - video/mj2
+  mp4:
+  - video/mp4
+  mp4v:
+  - video/mp4
+  mpg4:
+  - video/mp4
+  mpeg:
+  - video/mpeg
+  mpg:
+  - video/mpeg
+  mpe:
+  - video/mpeg
+  m1v:
+  - video/mpeg
+  m2v:
+  - video/mpeg
+  ogv:
+  - video/ogg
+  qt:
+  - video/quicktime
+  mov:
+  - video/quicktime
+  uvh:
+  - video/vnd.dece.hd
+  uvvh:
+  - video/vnd.dece.hd
+  uvm:
+  - video/vnd.dece.mobile
+  uvvm:
+  - video/vnd.dece.mobile
+  uvp:
+  - video/vnd.dece.pd
+  uvvp:
+  - video/vnd.dece.pd
+  uvs:
+  - video/vnd.dece.sd
+  uvvs:
+  - video/vnd.dece.sd
+  uvv:
+  - video/vnd.dece.video
+  uvvv:
+  - video/vnd.dece.video
+  dvb:
+  - video/vnd.dvb.file
+  fvt:
+  - video/vnd.fvt
+  mxu:
+  - video/vnd.mpegurl
+  m4u:
+  - video/vnd.mpegurl
+  pyv:
+  - video/vnd.ms-playready.media.pyv
+  uvu:
+  - video/vnd.uvvu.mp4
+  uvvu:
+  - video/vnd.uvvu.mp4
+  viv:
+  - video/vnd.vivo
+  webm:
+  - video/webm
+  f4v:
+  - video/x-f4v
+  fli:
+  - video/x-fli
+  flv:
+  - video/x-flv
+  m4v:
+  - video/x-m4v
+  mkv:
+  - video/x-matroska
+  mk3d:
+  - video/x-matroska
+  mks:
+  - video/x-matroska
+  mng:
+  - video/x-mng
+  asf:
+  - video/x-ms-asf
+  asx:
+  - video/x-ms-asf
+  vob:
+  - video/x-ms-vob
+  wm:
+  - video/x-ms-wm
+  wmv:
+  - video/x-ms-wmv
+  wmx:
+  - video/x-ms-wmx
+  wvx:
+  - video/x-ms-wvx
+  avi:
+  - video/x-msvideo
+  movie:
+  - video/x-sgi-movie
+  smv:
+  - video/x-smv
+  ice:
+  - x-conference/x-cooltalk

+ 2 - 2
system/config/permissions.yaml

@@ -96,7 +96,7 @@ cache:
   purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
   clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
   clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
-  clear_images_by_default: true                  # By default grav will include processed images in cache clear, this can be disabled
+  clear_images_by_default: false                  # By default grav does not include processed images in cache clear, this can be enabled
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   gzip: false                                    # GZip compress the page output
@@ -131,7 +131,7 @@ assets:                                          # Configuration for Assets Mana
   enable_asset_timestamp: false                  # Enable asset timestamps
   enable_asset_sri: false                        # Enable asset SRI
   collections:
-    jquery: system://assets/jquery/jquery-2.x.min.js
+    jquery: system://assets/jquery/jquery-3.x.min.js
 
 errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error

+ 1 - 1
system/defines.php

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

+ 11 - 0
system/images/media/thumb-3dm.png


+ 16 - 0
system/languages/bg.yaml

@@ -15,6 +15,7 @@ GRAV:
     BAD_DATE: Data invàlida
     AGO: abans
     FROM_NOW: des d'ara
+    JUST_NOW: Ara mateix
     SECOND: segon
     MINUTE: minut
     HOUR: hora
@@ -48,6 +49,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
     INVALID_INPUT: 'Entrada no vàlida a'
     MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
+    XSS_ISSUES: "Detectats potencials problemes XSS al camp '%s'"
   MONTHS_OF_THE_YEAR:
     - 'Gener'
     - 'Febrer'
@@ -69,3 +71,17 @@ GRAV:
     - 'Divendres'
     - 'Dissabte'
     - 'Diumenge'
+  YES: "Sí"
+  NO: "No"
+  CRON:
+    EVERY: cada
+    EVERY_HOUR: cada hora
+    EVERY_MINUTE: cada minut
+    EVERY_DAY_OF_WEEK: cada dia de la setmana
+    EVERY_DAY_OF_MONTH: cada dia del mes
+    EVERY_MONTH: cada mes
+    TEXT_PERIOD: Cada <b />
+    ERROR1: L'etiqueta %s no està suportada!
+    ERROR2: Nombre d'elements incorrecte
+    ERROR3: El jquery_element s'ha d'establir a la configuració de jqCron
+    ERROR4: Expressió no reconeguda

+ 9 - 5
system/languages/cs.yaml

@@ -24,6 +24,7 @@ GRAV:
     '/(quiz)zes$/i': '\1'
     '/(alias|status)es$/i': '\1'
     '/([octop|vir])i$/i': '\1us'
+    '/(n)ews$/i': '\1ouvelles'
   INFLECTOR_UNCOUNTABLE:
     - 'équipement'
     - 'information'
@@ -58,10 +59,10 @@ GRAV:
     MONTH: mois
     YEAR: année
     DECADE: décennie
-    SEC: s
-    MIN: m
-    HR: h
-    WK: sem
+    SEC: sec.
+    MIN: min.
+    HR: hr.
+    WK: sem.
     MO: m
     YR: an
     DEC: déc
@@ -84,6 +85,7 @@ GRAV:
     VALIDATION_FAIL: '<b>La validation a échoué :</b>'
     INVALID_INPUT: 'Saisie non valide'
     MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
+    XSS_ISSUES: "Erreurs XSS probablement détectées dans le champ '%s'"
   MONTHS_OF_THE_YEAR:
     - 'janvier'
     - 'février'
@@ -105,6 +107,8 @@ GRAV:
     - 'vendredi'
     - 'samedi'
     - 'dimanche'
+  YES: "Oui"
+  NO: "Non"
   CRON:
     EVERY: chaque
     EVERY_HOUR: toutes les heures
@@ -118,7 +122,7 @@ GRAV:
     TEXT_DOW: ' sur <b/>'
     TEXT_MONTH: ' de <b />'
     TEXT_DOM: ' sur <b/>'
-    ERROR1: La balise %s n'est pas supportée!
+    ERROR1: La balise %s n'est pas prise en charge !
     ERROR2: Nombre invalide d'éléments
     ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
     ERROR4: Expression non reconnue

+ 3 - 0
system/languages/gl.yaml

@@ -104,6 +104,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Fallou a validación:</b>'
     INVALID_INPUT: 'Entrada incorrecta en'
     MISSING_REQUIRED_FIELD: 'Falta un campo requirido:'
+    XSS_ISSUES: "Detectáronse posibles problemas XSS no campo '% s'"
   MONTHS_OF_THE_YEAR:
     - 'xaneiro'
     - 'febreiro'
@@ -125,6 +126,8 @@ GRAV:
     - 'venres'
     - 'sábado'
     - 'domingo'
+  YES: "Si"
+  NO: "Non"
   CRON:
     EVERY: cada
     EVERY_HOUR: Cada hora

+ 81 - 31
system/languages/he.yaml

@@ -3,26 +3,72 @@ GRAV:
   FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Frontmatter tidak valid\n\nLokasi: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
   INFLECTOR_PLURALS:
     '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
   INFLECTOR_UNCOUNTABLE:
-    - 'peralatan'
-    - 'informasi'
-    - 'nasi'
-    - 'uang'
-    - 'spesies'
-    - 'rangkaian'
-    - 'ikan'
-    - 'domba'
+    - 'Peralatan'
+    - 'Informasi '
+    - 'Nasi'
+    - 'Uang'
+    - 'Jenis'
+    - 'Seri'
+    - 'Ikan'
+    - 'Domba'
   INFLECTOR_IRREGULAR:
-    'person': 'orang-orang'
-    'man': 'laki-laki'
-    'child': 'anak-anak'
-    'sex': 'jenis kelamin'
+    'person': 'Orang-orang'
+    'man': 'Pria'
+    'child': 'Balita'
+    'sex': 'Jenis Kelamin'
     'move': 'pindahkan'
+  INFLECTOR_ORDINALS:
+    'default': 'ke'
+    'first': 'pertama'
+    'second': 'nd'
+    'third': 'rd'
   NICETIME:
-    NO_DATE_PROVIDED: Tanggal tidak tersedia
+    NO_DATE_PROVIDED: Tidak ada tanggal yang disediakan
     BAD_DATE: Format tanggal salah
     AGO: yang lalu
-    FROM_NOW: dari saat ini
+    FROM_NOW: dari sekarang
     JUST_NOW: baru saja
     SECOND: detik
     MINUTE: menit
@@ -32,12 +78,12 @@ GRAV:
     MONTH: bulan
     YEAR: tahun
     DECADE: dekade
-    SEC: dtk
-    MIN: mnt
-    HR: j
-    WK: mng
-    MO: bln
-    YR: thn
+    SEC: detik
+    MIN: menit
+    HR: ' jam'
+    WK: minggu
+    MO: bulan
+    YR: tahun
     DEC: desimal
     SECOND_PLURAL: detik
     MINUTE_PLURAL: menit
@@ -47,17 +93,18 @@ GRAV:
     MONTH_PLURAL: bulan
     YEAR_PLURAL: tahun
     DECADE_PLURAL: dekade
-    SEC_PLURAL: dtk
-    MIN_PLURAL: mnt
-    HR_PLURAL: j
-    WK_PLURAL: mgg
-    MO_PLURAL: bln
-    YR_PLURAL: thn
+    SEC_PLURAL: detik
+    MIN_PLURAL: menit
+    HR_PLURAL: jam
+    WK_PLURAL: minggu
+    MO_PLURAL: bulan
+    YR_PLURAL: tahun
     DEC_PLURAL: dekade
   FORM:
     VALIDATION_FAIL: '<b>Validasi gagal:</b>'
     INVALID_INPUT: 'Input tidak valid di'
     MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
+    XSS_ISSUES: "Isu berpotensial XSS terdeteksi dalam baris %s"
   MONTHS_OF_THE_YEAR:
     - 'Januari'
     - 'Februari'
@@ -76,22 +123,25 @@ GRAV:
     - 'Selasa'
     - 'Rabu'
     - 'Kamis'
-    - 'Jumat'
+    - 'Jum''at'
     - 'Sabtu'
     - 'Minggu'
+  YES: "Ya"
+  NO: "Tidak"
   CRON:
     EVERY: Setiap
     EVERY_HOUR: Setiap jam
     EVERY_MINUTE: Setiap menit
     EVERY_DAY_OF_WEEK: Setiap hari selama seminggu
-    EVERY_DAY_OF_MONTH: pada tanggal setiap bulannya
+    EVERY_DAY_OF_MONTH: Setiap hari dalam sebulan
     EVERY_MONTH: setiap bulan
     TEXT_PERIOD: Setiap <b />
+    TEXT_MINS: 'dalam <b />  menit setelah jam yang lalu'
     TEXT_TIME: ' pada <b />:<b />'
     TEXT_DOW: ' pada <b />'
     TEXT_MONTH: ' pada <b />'
     TEXT_DOM: ' pada <b />'
     ERROR1: Tag %s tidak didukung!
-    ERROR2: Jumlah elemen tidak valid
-    ERROR3: jquery_element harus ditetapkan ke pengaturan jqCron
-    ERROR4: Ekspresi tidak dikenali
+    ERROR2: Jumlah elemen yang buruk
+    ERROR3: jquery_element harus diatur ke dalam pengaturan jqCron
+    ERROR4: Ekspresi tidak dikenal

+ 147 - 0
system/languages/is.yaml

@@ -0,0 +1,147 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nГарчиг: %1$s\n---\n\n# Алдаа: Буруу Формат\n\nЗам: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1зүүд'
+    '/^(ox)$/i': '\1ууд'
+    '/([m|l])ouse$/i': '\1ууд'
+    '/(matr|vert|ind)ix|ex$/i': '\1иксүүд'
+    '/(x|ch|ss|sh)$/i': '\1үүд'
+    '/([^aeiouy]|qu)ies$/i': '\1үүд'
+    '/([^aeiouy]|qu)y$/i': '\1үүд'
+    '/(hive)$/i': '\1үүд'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2үүд'
+    '/sis$/i': 'үүд'
+    '/([ti])um$/i': '\1үүд'
+    '/(buffal|tomat)o$/i': '\1үүд'
+    '/(bu)s$/i': '\1үүд'
+    '/(alias|status)/i': '\1үүд'
+    '/(octop|vir)us$/i': '\1үүд'
+    '/(ax|test)is$/i': '\1үүд'
+    '/s$/i': 'үүд'
+    '/$/': 'үүд'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1икс'
+    '/(vert|ind)ices$/i': '\1икс'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1'
+    '/(cris|ax|test)es$/i': '\1'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1'
+    '/(s)eries$/i': '\1'
+    '/([^aeiouy]|qu)ies$/i': '\1үүд'
+    '/([lr])ves$/i': '\1'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1'
+    '/(^analy)ses$/i': '\1'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2үүд'
+    '/([ti])a$/i': '\1'
+    '/(n)ews$/i': '\1'
+  INFLECTOR_UNCOUNTABLE:
+    - 'тоног төхөөрөмж'
+    - 'Мэдээлэл'
+    - 'будаа'
+    - 'мөнгө'
+    - 'төрөл зүйл'
+    - 'цуврал'
+    - 'загас'
+    - 'хонь'
+  INFLECTOR_IRREGULAR:
+    'person': 'хүмүүс'
+    'man': 'эрчүүд'
+    'child': 'хүүхэд'
+    'sex': 'хүйс'
+    'move': 'хөдөлгөөн'
+  INFLECTOR_ORDINALS:
+    'default': 'th'
+    'first': 'st'
+    'second': 'nd'
+    'third': 'rd'
+  NICETIME:
+    NO_DATE_PROVIDED: Огноо алга
+    BAD_DATE: Буруу огноо
+    AGO: өмнө 
+    FROM_NOW: одооноос
+    JUST_NOW: дөнгөж сая
+    SECOND: секунд
+    MINUTE: минут
+    HOUR: цаг
+    DAY: өдөр
+    WEEK: долоо хоног
+    MONTH: сар
+    YEAR: он
+    DECADE: арван жил
+    SEC: сек
+    MIN: мин
+    HR: цаг
+    WK: д.х.
+    MO: сар
+    YR: он
+    DEC: арван жил
+    SECOND_PLURAL: секунд
+    MINUTE_PLURAL: минут
+    HOUR_PLURAL: цаг
+    DAY_PLURAL: өдрүүд
+    WEEK_PLURAL: долоо хоногууд
+    MONTH_PLURAL: сарууд
+    YEAR_PLURAL: онууд
+    DECADE_PLURAL: арван жилүүд
+    SEC_PLURAL: сек.-үүд
+    MIN_PLURAL: мин.-ууд
+    HR_PLURAL: цагууд
+    WK_PLURAL: д.х.-ууд
+    MO_PLURAL: сарууд
+    YR_PLURAL: жилүүд
+    DEC_PLURAL: арван жилүүд
+  FORM:
+    VALIDATION_FAIL: '<b>Баталгаажуулалт амжилтгүй боллоо:</b>'
+    INVALID_INPUT: 'Буруу өгөгдөл дараахид'
+    MISSING_REQUIRED_FIELD: 'Шаардлагатай талбар дутуу байна:'
+    XSS_ISSUES: "'%s' талбарт XSS -ийн болзошгүй асуудлууд илэрсэн"
+  MONTHS_OF_THE_YEAR:
+    - '1-р сар'
+    - '2-р сар'
+    - '3-р сар'
+    - '4-р сар'
+    - '5 сар'
+    - '6 сар'
+    - '7 сар'
+    - '8 сар'
+    - '9 сар'
+    - '10 сар'
+    - '11 сар'
+    - '12 сар'
+  DAYS_OF_THE_WEEK:
+    - 'Даваа гараг'
+    - 'Мягмар гараг'
+    - 'Лхагва гараг'
+    - 'Пүрэв гараг'
+    - 'Баасан гараг'
+    - 'Бямба гараг'
+    - 'Ням гараг'
+  YES: "Тийм"
+  NO: "Үгүй"
+  CRON:
+    EVERY: бүрийн
+    EVERY_HOUR: цаг бүрийн
+    EVERY_MINUTE: минут бүрийн
+    EVERY_DAY_OF_WEEK: долоо хоногийн өдөр болгонд
+    EVERY_DAY_OF_MONTH: сарын өдөр болгонд
+    EVERY_MONTH: сар болгон
+    TEXT_PERIOD: Бүрийн  <b />
+    TEXT_MINS: '  <b /> энэ сүүлийн цагийн минутад'
+    TEXT_TIME: '  <b />:<b /> -д'
+    TEXT_DOW: '  <b /> -д'
+    TEXT_MONTH: '  <b /> -ын'
+    TEXT_DOM: '  <b /> -т'
+    ERROR1: '%s -н утга нь дэмжигддэггүй!'
+    ERROR2: Элементүүдийн тоо хэмжээ буруу
+    ERROR3: jquery_element нь jqCron тохиргоонд хийгдсэн байх ёстой
+    ERROR4: Танигдаагүй илэрхийлэл

+ 3 - 0
system/languages/nb.yaml

@@ -104,6 +104,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Falha na validação:</b>'
     INVALID_INPUT: 'Dados inseridos são inválidos em'
     MISSING_REQUIRED_FIELD: 'Campo obrigatório em falta:'
+    XSS_ISSUES: "Potenciais problemas de XSS detectados no campo '%s'"
   MONTHS_OF_THE_YEAR:
     - 'Janeiro'
     - 'Fevereiro'
@@ -125,6 +126,8 @@ GRAV:
     - 'Sexta-feira'
     - 'Sábado'
     - 'Domingo'
+  YES: "Sim"
+  NO: "Não"
   CRON:
     EVERY: cada
     EVERY_HOUR: cada hora

+ 9 - 0
system/languages/ro.yaml

@@ -0,0 +1,9 @@
+---
+GRAV:
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/(x|ch|ss|sh)es$/i': '\1'

+ 2 - 0
system/languages/sk.yaml

@@ -82,6 +82,8 @@ GRAV:
     - 'Cuma'
     - 'Cumartesi'
     - 'Pazar'
+  YES: "Evet"
+  NO: "Hayır"
   CRON:
     EVERY: her
     EVERY_HOUR: saatte bir

+ 16 - 1
system/languages/uk.yaml

@@ -38,7 +38,9 @@ GRAV:
     YR_PLURAL: 年
     DEC_PLURAL: 十年
   FORM:
-    MISSING_REQUIRED_FIELD: 遺漏必填欄位:
+    VALIDATION_FAIL: '<b>確驗證失敗:</b>'
+    INVALID_INPUT: '無效輸入:'
+    MISSING_REQUIRED_FIELD: '遺漏必填欄位:'
   MONTHS_OF_THE_YEAR:
     - '一月'
     - '二月'
@@ -60,3 +62,16 @@ GRAV:
     - '星期五'
     - '星期六'
     - '星期日'
+  CRON:
+    EVERY: 每
+    EVERY_HOUR: 每小時
+    EVERY_MINUTE: 每分鐘
+    EVERY_DAY_OF_WEEK: 每一天
+    EVERY_DAY_OF_MONTH: 每一天
+    EVERY_MONTH: 每個月
+    TEXT_PERIOD: 每 <b />
+    TEXT_MINS: ' 的 <b /> 分'
+    TEXT_TIME: ' <b />:<b />'
+    TEXT_DOW: ' 的 <b />'
+    TEXT_MONTH: ' 的 <b />'
+    TEXT_DOM: ' 的 <b />'

+ 21 - 7
system/languages/zh.yaml

@@ -110,7 +110,7 @@ class Assets extends PropertyObject
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
         $this->assets_url = $locator->findResource('asset://', false);
 
         $this->config($asset_config);
@@ -164,10 +164,19 @@ class Assets extends PropertyObject
 
         // More than one asset
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                array_shift($args);
-                $args = array_merge([$a], $args);
-                call_user_func_array([$this, 'add'], $args);
+            foreach ($asset as $index => $location) {
+                $params = array_slice($args, 1);
+                if (is_array($location)) {
+                    $params = array_shift($params);
+                    if (is_numeric($params)) {
+                        $params = [ 'priority' => $params ];
+                    }
+                    $params = [array_replace_recursive([], $location, $params)];
+                    $location = $index;
+                }
+
+                $params = array_merge([$location], $params);
+                call_user_func_array([$this, 'add'], $params);
             }
         } elseif (isset($this->collections[$asset])) {
             array_shift($args);
@@ -201,8 +210,13 @@ class Assets extends PropertyObject
     protected function addType($collection, $type, $asset, $options)
     {
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                $this->addType($collection, $type, $a, $options);
+            foreach ($asset as $index => $location) {
+                $assetOptions = $options;
+                if (is_array($location)) {
+                    $assetOptions = array_replace_recursive([], $options, $location);
+                    $location = $index;
+                }
+                $this->addType($collection, $type, $location, $assetOptions);
             }
 
             return $this;

+ 17 - 7
system/src/Grav/Common/Assets/BaseAsset.php

@@ -15,6 +15,7 @@ use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
 use Grav\Framework\Object\PropertyObject;
+use RocketTheme\Toolbox\File\File;
 use SplFileInfo;
 
 /**
@@ -91,6 +92,10 @@ abstract class BaseAsset extends PropertyObject
      */
     public function init($asset, $options)
     {
+        if (!$asset) {
+            return false;
+        }
+
         $config = Grav::instance()['config'];
         $uri = Grav::instance()['uri'];
 
@@ -182,16 +187,21 @@ abstract class BaseAsset extends PropertyObject
     public static function integrityHash($input)
     {
         $grav = Grav::instance();
+        $uri = $grav['uri'];
 
         $assetsConfig = $grav['config']->get('system.assets');
 
-        if ( !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri'] )
-        {
-            $dataToHash = file_get_contents( GRAV_WEBROOT . $input);
+        if (!self::isRemoteLink($input) && !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri']) {
+            $input = preg_replace('#^' . $uri->rootUrl() . '#', '', $input);
+            $asset = File::instance(GRAV_WEBROOT . $input);
+
+            if ($asset->exists()) {
+                $dataToHash = $asset->content();
+                $hash = hash('sha256', $dataToHash, true);
+                $hash_base64 = base64_encode($hash);
 
-            $hash = hash('sha256', $dataToHash, true);
-            $hash_base64 = base64_encode($hash);
-            return ' integrity="sha256-' . $hash_base64 . '"';
+                return ' integrity="sha256-' . $hash_base64 . '"';
+            }
         }
 
         return '';
@@ -253,6 +263,6 @@ abstract class BaseAsset extends PropertyObject
      */
     protected function cssRewrite($file, $dir, $local)
     {
-        return;
+        return '';
     }
 }

+ 18 - 13
system/src/Grav/Common/Assets/Css.php

@@ -9,9 +9,9 @@
 
 namespace Grav\Common\Assets;
 
-use Grav\Common\Assets\BaseAsset;
 use Grav\Common\Assets\Traits\AssetUtilsTrait;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
@@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
         $uri = Grav::instance()['uri'];
 
         $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
+        if (!$this->assets_dir) {
+            // Attempt to create assets folder if it doesn't exist yet.
+            $this->assets_dir = $locator->findResource('asset://', true, true);
+            Folder::mkdir($this->assets_dir);
+            $locator->clearCache();
+        }
+
         $this->assets_url = $locator->findResource('asset://', false);
     }
 
@@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.css';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 
@@ -182,10 +188,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.js';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 
@@ -249,7 +254,7 @@ class Pipeline extends PropertyObject
                 $old_url = ltrim($old_url, '/');
             }
 
-            $new_url = ($local ? $this->base_url: '') . $old_url;
+            $new_url = ($local ? $this->base_url : '') . $old_url;
 
             return str_replace($matches[2], $new_url, $matches[0]);
         }, $file);

+ 6 - 4
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php

@@ -68,8 +68,6 @@ trait AssetUtilsTrait
     protected function gatherLinks(array $assets, $css = true)
     {
         $buffer = '';
-
-
         foreach ($assets as $id => $asset) {
             $local = true;
 
@@ -135,7 +133,7 @@ trait AssetUtilsTrait
 
         $imports = [];
 
-        $file = (string)preg_replace_callback($regex, function ($matches) use (&$imports) {
+        $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) {
             $imports[] = $matches[0];
 
             return '';
@@ -156,6 +154,10 @@ trait AssetUtilsTrait
         $no_key = ['loading'];
 
         foreach ($this->attributes as $key => $value) {
+            if ($value === null) {
+                continue;
+            }
+
             if (is_numeric($key)) {
                 $key = $value;
             }
@@ -196,7 +198,7 @@ trait AssetUtilsTrait
         }
 
         if ($this->timestamp) {
-            if (Utils::contains($asset, '?') || $querystring) {
+            if ($querystring || Utils::contains($asset, '?')) {
                 $querystring .=  '&' . $this->timestamp;
             } else {
                 $querystring .= '?' . $this->timestamp;

+ 1 - 1
system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php

@@ -222,7 +222,7 @@ class Backups
             $backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
         }
 
-        if (!file_exists($backup_root)) {
+        if (!$backup_root || !file_exists($backup_root)) {
             throw new RuntimeException("Backup location: {$backup_root} does not exist...");
         }
 

+ 2 - 6
system/src/Grav/Common/Browser.php

@@ -141,7 +141,7 @@ class Cache extends Getters
         $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
 
         // Cache key allows us to invalidate all cache on configuration changes.
-        $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
+        $this->key = ($prefix ?: 'g') . '-' . $uniqueness;
         $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
         $this->driver_setting = $this->config->get('system.cache.driver');
         $this->driver = $this->getCacheDriver();
@@ -618,11 +618,7 @@ class Cache extends Getters
      */
     public function isVolatileDriver($setting)
     {
-        if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
-            return true;
-        }
-
-        return false;
+        return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true);
     }
 
     /**

+ 13 - 3
system/src/Grav/Common/Composer.php

@@ -41,6 +41,9 @@ class Setup extends Data
      */
     public static $environment;
 
+    /** @var string */
+    public static $securityFile = 'config://security.yaml';
+
     /** @var array */
     protected $streams = [
         'user' => [
@@ -390,12 +393,19 @@ class Setup extends Data
 
             if (!$locator->findResource('environment://config', true)) {
                 // If environment does not have its own directory, remove it from the lookup.
-                $this->set('streams.schemes.environment.prefixes', ['config' => []]);
+                $prefixes = $this->get('streams.schemes.environment.prefixes');
+                $prefixes['config'] = [];
+
+                $this->set('streams.schemes.environment.prefixes', $prefixes);
                 $this->initializeLocator($locator);
             }
 
-            // Create security.yaml if it doesn't exist.
-            $filename = $locator->findResource('config://security.yaml', true, true);
+            // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
+            $securityFile = basename(static::$securityFile);
+            $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
+            $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
+            $filename = "{$securityFolder}/{$securityFile}";
+
             $security_file = CompiledYamlFile::instance($filename);
             $security_content = (array)$security_file->content();
 

+ 2 - 2
system/src/Grav/Common/Data/Blueprint.php

@@ -37,7 +37,7 @@ class Blueprint extends BlueprintForm
     /** @var string|null */
     protected $scope;
 
-    /** @var BlueprintSchema */
+    /** @var BlueprintSchema|null */
     protected $blueprintSchema;
 
     /** @var object|null */
@@ -54,7 +54,7 @@ class Blueprint extends BlueprintForm
      */
     public function __clone()
     {
-        if ($this->blueprintSchema) {
+        if (null !== $this->blueprintSchema) {
             $this->blueprintSchema = clone $this->blueprintSchema;
         }
     }

+ 13 - 0
system/src/Grav/Common/Data/BlueprintSchema.php

@@ -56,6 +56,15 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         return $this->types[$name] ?? [];
     }
 
+    /**
+     * @param string $name
+     * @return array|null
+     */
+    public function getNestedRules(string $name)
+    {
+        return $this->getNested($name);
+    }
+
     /**
      * Validate data against blueprints.
      *
@@ -317,6 +326,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                     $toggle = [];
                 }
                 // Recursively fetch the items.
+                $childData = $data[$key] ?? null;
+                if (null !== $childData && !is_array($childData)) {
+                    throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
+                }
                 $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
             } else {
                 $field = $this->get($value);

+ 1 - 1
system/src/Grav/Common/Data/Blueprints.php

@@ -264,7 +264,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
      */
     public function blueprints()
     {
-        if (!$this->blueprints) {
+        if (null === $this->blueprints) {
             $this->blueprints = new Blueprint();
         } elseif (is_callable($this->blueprints)) {
             // Lazy load blueprints.

+ 23 - 8
system/src/Grav/Common/Data/DataInterface.php

@@ -519,17 +519,32 @@ class Validation
             return false;
         }
 
-        if (isset($params['min']) && $value < $params['min']) {
-            return false;
+        $value = (float)$value;
+
+        $min = 0;
+        if (isset($params['min'])) {
+            $min = (float)$params['min'];
+            if ($value < $min) {
+                return false;
+            }
         }
 
-        if (isset($params['max']) && $value > $params['max']) {
-            return false;
+        if (isset($params['max'])) {
+            $max = (float)$params['max'];
+            if ($value > $max) {
+                return false;
+            }
         }
 
-        $min = $params['min'] ?? 0;
+        if (isset($params['step'])) {
+            $step = (float)$params['step'];
+            // Count of how many steps we are above/below the minimum value.
+            $pos = ($value - $min) / $step;
 
-        return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
+            return is_int(static::filterNumber($pos, $params, $field));
+        }
+
+        return true;
     }
 
     /**
@@ -593,7 +608,7 @@ class Validation
      */
     public static function typeColor($value, array $params, array $field)
     {
-        return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
+        return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
     }
 
     /**
@@ -1174,7 +1189,7 @@ class Validation
      */
     public static function filterItem_List($value, $params)
     {
-        return array_values(array_filter($value, function ($v) {
+        return array_values(array_filter($value, static function ($v) {
             return !empty($v);
         }));
     }

+ 1 - 1
system/src/Grav/Common/Data/ValidationException.php

@@ -332,7 +332,7 @@ class Debugger
             return new Response(404, $headers, json_encode($response));
         }
 
-        $data = is_array($data) ? array_map(function ($item) {
+        $data = is_array($data) ? array_map(static function ($item) {
             return $item->toArray();
         }, $data) : $data->toArray();
 

+ 4 - 3
system/src/Grav/Common/Errors/BareHandler.php

@@ -197,7 +197,7 @@ abstract class Folder
      * Shift first directory out of the path.
      *
      * @param string $path
-     * @return string
+     * @return string|null
      */
     public static function shift(&$path)
     {
@@ -371,7 +371,7 @@ abstract class Folder
             return;
         }
 
-        if (strpos($target, $source) === 0) {
+        if (strpos($target, $source . '/') === 0) {
             throw new RuntimeException('Cannot move folder to itself');
         }
 
@@ -417,7 +417,8 @@ abstract class Folder
 
         if (!$success) {
             $error = error_get_last();
-            throw new RuntimeException($error['message']);
+
+            throw new RuntimeException($error['message'] ?? 'Unknown error');
         }
 
         // Make sure that the change will be detected when caching.

+ 3 - 4
system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php

@@ -57,7 +57,9 @@ class ZipArchiver extends Archiver
             throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
         }
 
-        if (!file_exists($source)) {
+        // Get real path for our folder
+        $rootPath = realpath($source);
+        if (!$rootPath) {
             throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
         }
 
@@ -66,9 +68,6 @@ class ZipArchiver extends Archiver
             throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
         }
 
-        // Get real path for our folder
-        $rootPath = realpath($source);
-
         $files = $this->getArchiveFiles($rootPath);
 
         $status && $status([

+ 3 - 2
system/src/Grav/Common/Flex/FlexCollection.php

@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
 
 use Grav\Common\Flex\Traits\FlexGravTrait;
 use Grav\Common\Flex\Traits\FlexObjectTrait;
+use Grav\Common\Media\Interfaces\MediaInterface;
 use Grav\Framework\Flex\Traits\FlexMediaTrait;
 use function is_array;
 
@@ -21,7 +22,7 @@ use function is_array;
  *
  * @package Grav\Common\Flex
  */
-abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
+abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
 {
     use FlexGravTrait;
     use FlexObjectTrait;
@@ -42,7 +43,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
 
         // Handle media fields.
         $settings = $this->getFieldSettings($name);
-        if ($settings['media_field'] ?? false === true) {
+        if (($settings['media_field'] ?? false) === true) {
             return $this->parseFileProperty($value, $settings);
         }
 

+ 19 - 12
system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php

@@ -19,7 +19,6 @@ use Grav\Common\Page\Header;
 use Grav\Common\Page\Interfaces\PageCollectionInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Utils;
-use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 use Grav\Framework\Flex\Pages\FlexPageCollection;
 use Collator;
 use InvalidArgumentException;
@@ -159,7 +158,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      */
     public function addPage(PageInterface $page)
     {
-        if (!$page instanceof FlexObjectInterface) {
+        if (!$page instanceof PageObject) {
             throw new InvalidArgumentException('$page is not a flex page.');
         }
 
@@ -192,6 +191,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
         throw new RuntimeException(__METHOD__ . '(): Not Implemented');
     }
 
+    /**
+     * Set current page.
+     */
+    public function setCurrent(string $path): void
+    {
+        throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+    }
+
     /**
      * Return previous item.
      *
@@ -392,8 +399,8 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
             $i = count($manual);
             $new_list = [];
             foreach ($list as $key => $dummy) {
-                $child = $this[$key];
-                $order = array_search($child->slug, $manual, true);
+                $child = $this[$key] ?? null;
+                $order = $child ? array_search($child->slug, $manual, true) : false;
                 if ($order === false) {
                     $order = $i++;
                 }
@@ -426,20 +433,20 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
 
     /**
      * Returns the items between a set of date ranges of either the page date field (default) or
-     * an arbitrary datetime page field where end date is optional
-     * Dates can be passed in as text that strtotime() can process
+     * an arbitrary datetime page field where start date and end date are optional
+     * Dates must be passed in as text that strtotime() can process
      * http://php.net/manual/en/function.strtotime.php
      *
-     * @param string $startDate
-     * @param string|false $endDate
+     * @param string|null $startDate
+     * @param string|null $endDate
      * @param string|null $field
      * @return static
      * @throws Exception
      */
-    public function dateRange($startDate, $endDate = false, $field = null)
+    public function dateRange($startDate = null, $endDate = null, $field = null)
     {
-        $start = Utils::date2timestamp($startDate);
-        $end = $endDate ? Utils::date2timestamp($endDate) : false;
+        $start = $startDate ? Utils::date2timestamp($startDate) : null;
+        $end = $endDate ? Utils::date2timestamp($endDate) : null;
 
         $entries = [];
         foreach ($this as $key => $object) {
@@ -449,7 +456,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
 
             $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
 
-            if ($date >= $start && (!$end || $date <= $end)) {
+            if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
                 $entries[$key] = $object;
             }
         }

+ 20 - 13
system/src/Grav/Common/Flex/Types/Pages/PageIndex.php

@@ -109,6 +109,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
         }
 
         $element = parent::get($key);
+        if (null === $element) {
+            return null;
+        }
+
         if (isset($params)) {
             $element = $element->getTranslation(ltrim($params, '.'));
         }
@@ -331,7 +335,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      */
     protected function filterByParent(array $filters)
     {
-        return parent::filterBy($filters);
+        /** @var static $index */
+        $index = parent::filterBy($filters);
+
+        return $index;
     }
 
     /**
@@ -673,13 +680,14 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                     $child_count = $tmp->count();
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
                     $route = $child->getRoute();
+                    $route = $route ? ($route->toString(false) ?: '/') : '';
                     $payload = [
-                        'item-key' => basename($child->rawRoute() ?? $child->getKey()),
+                        'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
                         'icon' => $icon,
                         'title' => htmlspecialchars($child->menu()),
                         'route' => [
-                            'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
-                            'raw' => $child->rawRoute(),
+                            'display' => htmlspecialchars($route) ?: null,
+                            'raw' => htmlspecialchars($child->rawRoute()),
                         ],
                         'modified' => $this->jsDate($child->modified()),
                         'child_count' => $child_count ?: null,
@@ -834,12 +842,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
     /**
      * Remove item from the list.
      *
-     * @param PageInterface|string|null $key
-     *
-     * @return $this
+     * @param string $key
+     * @return PageObject|null
      * @throws InvalidArgumentException
      */
-    public function remove($key = null)
+    public function remove($key)
     {
         return $this->getCollection()->remove($key);
     }
@@ -949,17 +956,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
 
     /**
      * Returns the items between a set of date ranges of either the page date field (default) or
-     * an arbitrary datetime page field where end date is optional
-     * Dates can be passed in as text that strtotime() can process
+     * an arbitrary datetime page field where start date and end date are optional
+     * Dates must be passed in as text that strtotime() can process
      * http://php.net/manual/en/function.strtotime.php
      *
-     * @param string $startDate
-     * @param bool $endDate
+     * @param string|null $startDate
+     * @param string|null $endDate
      * @param string|null $field
      * @return static
      * @throws Exception
      */
-    public function dateRange($startDate, $endDate = false, $field = null)
+    public function dateRange($startDate = null, $endDate = null, $field = null)
     {
         $collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
 

+ 46 - 12
system/src/Grav/Common/Flex/Types/Pages/PageObject.php

@@ -104,12 +104,12 @@ class PageObject extends FlexPageObject
      */
     public function getRoute($query = []): ?Route
     {
-        $route = $this->route();
-        if (null === $route) {
+        $path = $this->route();
+        if (null === $path) {
             return null;
         }
 
-        $route = RouteFactory::createFromString($route);
+        $route = RouteFactory::createFromString($path);
         if ($lang = $route->getLanguage()) {
             $grav = Grav::instance();
             if (!$grav['config']->get('system.languages.include_default_lang')) {
@@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
         $this->getFlexDirectory()->reloadIndex();
     }
 
+    /**
+     * @param UserInterface|null $user
+     */
+    public function check(UserInterface $user = null): void
+    {
+        parent::check($user);
+
+        if ($user && $this->isMoved()) {
+            $parentKey = $this->getProperty('parent_key');
+
+            /** @var PageObject|null $parent */
+            $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
+            if (!$parent || !$parent->isAuthorized('create', null, $user)) {
+                throw new \RuntimeException('Forbidden', 403);
+            }
+        }
+    }
+
     /**
      * @param array|bool $reorder
      * @return FlexObject|FlexObjectInterface
@@ -293,7 +311,7 @@ class PageObject extends FlexPageObject
         }
 
         // Reset original after save events have all been called.
-        $this->_original = null;
+        $this->_originalObject = null;
 
         return $instance;
     }
@@ -358,16 +376,26 @@ class PageObject extends FlexPageObject
     }
 
     /**
-     * @param array $ordering
-     * @return PageCollection|null
+     * @return bool
      */
-    protected function reorderSiblings(array $ordering)
+    protected function isMoved(): bool
     {
         $storageKey = $this->getMasterKey();
         $filesystem = Filesystem::getInstance(false);
         $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
         $newParentKey = $this->getProperty('parent_key');
-        $isMoved = $oldParentKey !== $newParentKey;
+
+        return $this->exists() && $oldParentKey !== $newParentKey;
+    }
+
+    /**
+     * @param array $ordering
+     * @return PageCollection|null
+     */
+    protected function reorderSiblings(array $ordering)
+    {
+        $storageKey = $this->getMasterKey();
+        $isMoved = $this->isMoved();
         $order = !$isMoved ? $this->order() : false;
         if ($order !== false) {
             $order = (int)$order;
@@ -385,10 +413,12 @@ class PageObject extends FlexPageObject
         // Handle special case where ordering isn't given.
         if ($ordering === []) {
             if ($order >= 999999) {
-                // Set ordering to point to be the last item.
+                // Set ordering to point to be the last item, ignoring the object itself.
                 $order = 0;
                 foreach ($siblings as $sibling) {
-                    $order = max($order, (int)$sibling->order());
+                    if ($sibling->getKey() !== $this->getKey()) {
+                        $order = max($order, (int)$sibling->order());
+                    }
                 }
                 $this->order($order + 1);
             }
@@ -411,7 +441,8 @@ class PageObject extends FlexPageObject
 
         // Add missing siblings into the end of the list, keeping the previous ordering between them.
         foreach ($siblings as $sibling) {
-            $basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
+            $folder = (string)$sibling->getProperty('folder');
+            $basename = preg_replace('|^\d+\.|', '', $folder);
             if (!in_array($basename, $ordering, true)) {
                 $ordering[] = $basename;
             }
@@ -421,7 +452,8 @@ class PageObject extends FlexPageObject
         $ordering = array_flip(array_values($ordering));
         $count = count($ordering);
         foreach ($siblings as $sibling) {
-            $basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
+            $folder = (string)$sibling->getProperty('folder');
+            $basename = preg_replace('|^\d+\.|', '', $folder);
             $newOrder = $ordering[$basename] ?? null;
             $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
             $sibling->order($newOrder);
@@ -500,6 +532,8 @@ class PageObject extends FlexPageObject
         if ($isNew === true && $name === '') {
             // Support onBlueprintCreated event just like in Pages::blueprints($template)
             $blueprint->set('initialized', true);
+            $blueprint->setFilename($template);
+
             Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
         }
 

+ 4 - 1
system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php

@@ -103,7 +103,10 @@ trait PageLegacyTrait
         $parent = $this->parent();
         $collection = $parent ? $parent->collection('content', false) : null;
         if (null !== $path && $collection instanceof PageCollectionInterface) {
-            return $collection->adjacentSibling($path, $direction);
+            $child = $collection->adjacentSibling($path, $direction);
+            if ($child instanceof PageInterface) {
+                return $child;
+            }
         }
 
         return false;

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

@@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
         ] + parent::getCachedMethods();
     }
 
+    /**
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return $this->getProperty('readableName');
+    }
+
     /**
      * Checks user authorization to the action.
      *

+ 2 - 2
system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php

@@ -92,7 +92,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
                 } else {
                     $user = parent::find($query, $field);
                 }
-                if ($user) {
+                if ($user instanceof UserObject) {
                     return $user;
                 }
             }
@@ -123,7 +123,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
      * @param string $key
      * @return string
      */
-    protected function filterUsername(string $key)
+    protected function filterUsername(string $key): string
     {
         $storage = $this->getFlexDirectory()->getStorage();
         if (method_exists($storage, 'normalizeKey')) {

+ 2 - 2
system/src/Grav/Common/Flex/Types/Users/UserIndex.php

@@ -62,7 +62,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
      * @param FlexStorageInterface $storage
      * @return void
      */
-    public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage)
+    public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage): void
     {
         // Username can also be number and stored as such.
         $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
@@ -187,7 +187,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
      * @param array $updated
      * @param array $removed
      */
-    protected static function onChanges(array $entries, array $added, array $updated, array $removed)
+    protected static function onChanges(array $entries, array $added, array $updated, array $removed): void
     {
         $message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed));
 

+ 39 - 5
system/src/Grav/Common/Flex/Types/Users/UserObject.php

@@ -11,6 +11,7 @@ declare(strict_types=1);
 
 namespace Grav\Common\Flex\Types\Users;
 
+use Closure;
 use Countable;
 use Grav\Common\Config\Config;
 use Grav\Common\Data\Blueprint;
@@ -31,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Common\User\Traits\UserTrait;
 use Grav\Framework\File\Formatter\JsonFormatter;
 use Grav\Framework\File\Formatter\YamlFormatter;
+use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\Flex;
 use Grav\Framework\Flex\FlexDirectory;
 use Grav\Framework\Flex\Storage\FileStorage;
@@ -75,6 +77,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
     use UserTrait;
     use UserObjectLegacyTrait;
 
+    /** @var Closure|null */
+    static public $authorizeCallable;
+
     /** @var array|null */
     protected $_uploads_original;
     /** @var FileInterface|null */
@@ -259,6 +264,15 @@ class UserObject extends FlexObject implements UserInterface, Countable
             }
         }
 
+        $authorizeCallable = static::$authorizeCallable;
+        if ($authorizeCallable instanceof Closure) {
+            $authorizeCallable->bindTo($this);
+            $authorized = $authorizeCallable($action, $scope);
+            if (is_bool($authorized)) {
+                return $authorized;
+            }
+        }
+
         // Check user access.
         $access = $this->getAccess();
         $authorized = $access->authorize($action, $scope);
@@ -292,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
         return $value;
     }
 
+    /**
+     * @return UserGroupIndex
+     */
+    public function getRoles(): UserGroupIndex
+    {
+        return $this->getGroups();
+    }
+
     /**
      * Convert object into an array.
      *
@@ -689,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
 
     /**
      * @param array $files
+     * @return void
      */
     protected function setUpdatedMedia(array $files): void
     {
@@ -696,10 +719,16 @@ class UserObject extends FlexObject implements UserInterface, Countable
         $locator = Grav::instance()['locator'];
 
         $media = $this->getMedia();
+        if (!$media instanceof MediaUploadInterface) {
+            return;
+        }
+
+        $filesystem = Filesystem::getInstance(false);
 
         $list = [];
         $list_original = [];
         foreach ($files as $field => $group) {
+            // Ignore files without a field.
             if ($field === '') {
                 continue;
             }
@@ -707,7 +736,6 @@ class UserObject extends FlexObject implements UserInterface, Countable
 
             // Load settings for the field.
             $settings = $this->getMediaFieldSettings($field);
-
             foreach ($group as $filename => $file) {
                 if ($file) {
                     // File upload.
@@ -722,8 +750,8 @@ class UserObject extends FlexObject implements UserInterface, Countable
                 }
 
                 if ($file) {
-                    // Check file upload against media limits.
-                    $filename = $media->checkUploadedFile($file, $filename, $settings);
+                    // Check file upload against media limits (except for max size).
+                    $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
                 }
 
                 $self = $settings['self'];
@@ -746,19 +774,25 @@ class UserObject extends FlexObject implements UserInterface, Countable
                     continue;
                 }
 
+                // Calculate path without the retina scaling factor.
+                $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
+
                 $list[$filename] = [$file, $settings];
 
+                $path = str_replace('.', "\n", $field);
                 if (null !== $data) {
                     $data['name'] = $filename;
                     $data['path'] = $filepath;
 
-                    $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
+                    $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
                 } else {
-                    $this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
+                    $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
                 }
             }
         }
 
+        $this->clearMediaCache();
+
         $this->_uploads = $list;
         $this->_uploads_original = $list_original;
     }

+ 99 - 37
system/src/Grav/Common/Form/FormFlash.php

@@ -35,7 +35,11 @@ class GPM extends Iterator
     /** @var Remote\Packages|null Remote available Packages */
     private $repository;
     /** @var Remote\GravCore|null Remove Grav Packages */
-    public $grav;
+    private $grav;
+    /** @var bool */
+    private $refresh;
+    /** @var callable|null */
+    private $callback;
 
     /** @var array Internal cache */
     protected $cache;
@@ -55,13 +59,45 @@ class GPM extends Iterator
     public function __construct($refresh = false, $callback = null)
     {
         parent::__construct();
+
+        Folder::create(CACHE_DIR . '/gpm');
+
         $this->cache = [];
         $this->installed = new Local\Packages();
-        try {
-            $this->repository = new Remote\Packages($refresh, $callback);
-            $this->grav = new Remote\GravCore($refresh, $callback);
-        } catch (Exception $e) {
+        $this->refresh = $refresh;
+        $this->callback = $callback;
+    }
+
+    /**
+     * Magic getter method
+     *
+     * @param string $offset Asset name value
+     * @return mixed Asset value
+     */
+    public function __get($offset)
+    {
+        switch ($offset) {
+            case 'grav':
+                return $this->getGrav();
+        }
+
+        return parent::__get($offset);
+    }
+
+    /**
+     * Magic method to determine if the attribute is set
+     *
+     * @param string $offset Asset name value
+     * @return bool True if the value is set
+     */
+    public function __isset($offset)
+    {
+        switch ($offset) {
+            case 'grav':
+                return $this->getGrav() !== null;
         }
+
+        return parent::__isset($offset);
     }
 
     /**
@@ -266,11 +302,12 @@ class GPM extends Iterator
     {
         $items = [];
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
         }
 
-        $repository = $this->repository['plugins'];
+        $plugins = $repository['plugins'];
 
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
@@ -278,18 +315,18 @@ class GPM extends Iterator
         }
 
         foreach ($this->installed['plugins'] as $slug => $plugin) {
-            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+            if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
                 continue;
             }
 
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $plugins[$slug]->version;
 
             if (version_compare($local_version, $remote_version) < 0) {
-                $repository[$slug]->available = $remote_version;
-                $repository[$slug]->version = $local_version;
-                $repository[$slug]->type = $repository[$slug]->release_type;
-                $items[$slug] = $repository[$slug];
+                $plugins[$slug]->available = $remote_version;
+                $plugins[$slug]->version = $local_version;
+                $plugins[$slug]->type = $plugins[$slug]->release_type;
+                $items[$slug] = $plugins[$slug];
             }
         }
 
@@ -306,19 +343,20 @@ class GPM extends Iterator
      */
     public function getLatestVersionOfPackage($package_name)
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
         }
 
-        $repository = $this->repository['plugins'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->available ?: $repository[$package_name]->version;
+        $plugins = $repository['plugins'];
+        if (isset($plugins[$package_name])) {
+            return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
         }
 
         //Not a plugin, it's a theme?
-        $repository = $this->repository['themes'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->available ?: $repository[$package_name]->version;
+        $themes = $repository['themes'];
+        if (isset($themes[$package_name])) {
+            return $themes[$package_name]->available ?: $themes[$package_name]->version;
         }
 
         return null;
@@ -356,11 +394,12 @@ class GPM extends Iterator
     {
         $items = [];
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
         }
 
-        $repository = $this->repository['themes'];
+        $themes = $repository['themes'];
 
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
@@ -368,18 +407,18 @@ class GPM extends Iterator
         }
 
         foreach ($this->installed['themes'] as $slug => $plugin) {
-            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+            if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
                 continue;
             }
 
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $themes[$slug]->version;
 
             if (version_compare($local_version, $remote_version) < 0) {
-                $repository[$slug]->available = $remote_version;
-                $repository[$slug]->version = $local_version;
-                $repository[$slug]->type = $repository[$slug]->release_type;
-                $items[$slug] = $repository[$slug];
+                $themes[$slug]->available = $remote_version;
+                $themes[$slug]->version = $local_version;
+                $themes[$slug]->type = $themes[$slug]->release_type;
+                $items[$slug] = $themes[$slug];
             }
         }
 
@@ -407,19 +446,20 @@ class GPM extends Iterator
      */
     public function getReleaseType($package_name)
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
         }
 
-        $repository = $this->repository['plugins'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->release_type;
+        $plugins = $repository['plugins'];
+        if (isset($plugins[$package_name])) {
+            return $plugins[$package_name]->release_type;
         }
 
         //Not a plugin, it's a theme?
-        $repository = $this->repository['themes'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->release_type;
+        $themes = $repository['themes'];
+        if (isset($themes[$package_name])) {
+            return $themes[$package_name]->release_type;
         }
 
         return null;
@@ -470,7 +510,7 @@ class GPM extends Iterator
      */
     public function getRepositoryPlugins()
     {
-        return $this->repository['plugins'] ?? null;
+        return $this->getRepository()['plugins'] ?? null;
     }
 
     /**
@@ -493,7 +533,7 @@ class GPM extends Iterator
      */
     public function getRepositoryThemes()
     {
-        return $this->repository['themes'] ?? null;
+        return $this->getRepository()['themes'] ?? null;
     }
 
     /**
@@ -504,9 +544,31 @@ class GPM extends Iterator
      */
     public function getRepository()
     {
+        if (null === $this->repository) {
+            try {
+                $this->repository = new Remote\Packages($this->refresh, $this->callback);
+            } catch (Exception $e) {}
+        }
+
         return $this->repository;
     }
 
+    /**
+     * Returns Grav version available in the repository
+     *
+     * @return Remote\GravCore|null
+     */
+    public function getGrav()
+    {
+        if (null === $this->grav) {
+            try {
+                $this->grav = new Remote\GravCore($this->refresh, $this->callback);
+            } catch (Exception $e) {}
+        }
+
+        return $this->grav;
+    }
+
     /**
      * Searches for a Package in the repository
      *

+ 19 - 13
system/src/Grav/Common/GPM/Installer.php

@@ -9,6 +9,7 @@
 
 namespace Grav\Common;
 
+use Composer\Autoload\ClassLoader;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Setup;
 use Grav\Common\Helpers\Exif;
@@ -134,7 +135,7 @@ class Grav extends Container
      *
      * @return void
      */
-    public static function resetInstance()
+    public static function resetInstance(): void
     {
         if (self::$instance) {
             // @phpstan-ignore-next-line
@@ -152,6 +153,13 @@ class Grav extends Container
     {
         if (null === self::$instance) {
             self::$instance = static::load($values);
+
+            /** @var ClassLoader|null $loader */
+            $loader = self::$instance['loader'] ?? null;
+            if ($loader) {
+                // Load fix for Deferred Twig Extension
+                $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
+            }
         } elseif ($values) {
             $instance = self::$instance;
             foreach ($values as $key => $value) {
@@ -234,7 +242,7 @@ class Grav extends Container
      *
      * @return void
      */
-    public function process()
+    public function process(): void
     {
         if (isset($this->initialized['process'])) {
             return;
@@ -466,7 +474,7 @@ class Grav extends Container
      * @param int    $code  Redirection code (30x)
      * @return void
      */
-    public function redirectLangSafe($route, $code = null)
+    public function redirectLangSafe($route, $code = null): void
     {
         if (!$this['uri']->isExternal($route)) {
             $this->redirect($this['pages']->route($route), $code);
@@ -481,7 +489,7 @@ class Grav extends Container
      * @param ResponseInterface|null $response
      * @return void
      */
-    public function header(ResponseInterface $response = null)
+    public function header(ResponseInterface $response = null): void
     {
         if (null === $response) {
             /** @var PageInterface $page */
@@ -506,7 +514,7 @@ class Grav extends Container
      *
      * @return void
      */
-    public function setLocale()
+    public function setLocale(): void
     {
         // Initialize Locale if set and configured.
         if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
@@ -567,7 +575,7 @@ class Grav extends Container
      *
      * @return void
      */
-    public function shutdown()
+    public function shutdown(): void
     {
         // Prevent user abort allowing onShutdown event to run without interruptions.
         if (function_exists('ignore_user_abort')) {
@@ -686,7 +694,7 @@ class Grav extends Container
      *
      * @return void
      */
-    protected function registerServices()
+    protected function registerServices(): void
     {
         foreach (self::$diMap as $serviceKey => $serviceClass) {
             if (is_int($serviceKey)) {
@@ -753,12 +761,10 @@ class Grav extends Container
             // unsupported media type, try to download it...
             if ($uri_extension) {
                 $extension = $uri_extension;
+            } elseif (isset($path_parts['extension'])) {
+                $extension = $path_parts['extension'];
             } else {
-                if (isset($path_parts['extension'])) {
-                    $extension = $path_parts['extension'];
-                } else {
-                    $extension = null;
-                }
+                $extension = null;
             }
 
             if ($extension) {
@@ -773,6 +779,6 @@ class Grav extends Container
             return false;
         }
 
-        return $page;
+        return $page ?? false;
     }
 }

+ 6 - 1
system/src/Grav/Common/GravTrait.php

@@ -33,6 +33,9 @@ class Excerpts
     public static function processImageHtml($html, PageInterface $page = null)
     {
         $excerpt = static::getExcerptFromHtml($html, 'img');
+        if (null === $excerpt) {
+            return '';
+        }
 
         $original_src = $excerpt['element']['attributes']['src'];
         $excerpt['element']['attributes']['href'] = $original_src;
@@ -61,6 +64,9 @@ class Excerpts
     public static function processLinkHtml($html, PageInterface $page = null)
     {
         $excerpt = static::getExcerptFromHtml($html, 'a');
+        if (null === $excerpt) {
+            return '';
+        }
 
         $original_href = $excerpt['element']['attributes']['href'];
         $excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
@@ -89,7 +95,6 @@ class Excerpts
         $excerpt = null;
         $inner = [];
 
-        /** @var DOMElement $element */
         foreach ($elements as $element) {
             $attributes = [];
             foreach ($element->attributes as $name => $value) {

+ 12 - 10
system/src/Grav/Common/Helpers/Exif.php

@@ -53,7 +53,6 @@ class LogViewer
      */
     public function tail($filepath, $lines = 1)
     {
-
         $f = $filepath ? @fopen($filepath, 'rb') : false;
         if ($f === false) {
             return false;
@@ -62,13 +61,12 @@ class LogViewer
         $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
 
         fseek($f, -1, SEEK_END);
-        if (fread($f, 1) != "\n") {
-            $lines -= 1;
+        if (fread($f, 1) !== "\n") {
+            --$lines;
         }
 
         // Start reading
         $output = '';
-        $chunk = '';
         // While we would like more
         while (ftell($f) > 0 && $lines >= 0) {
             // Figure out how far back we should jump
@@ -76,7 +74,11 @@ class LogViewer
             // Do the jump (backwards, relative to where we are)
             fseek($f, -$seek, SEEK_CUR);
             // Read a chunk and prepend it to our output
-            $output = ($chunk = fread($f, $seek)) . $output;
+            $chunk = fread($f, $seek);
+            if ($chunk === false) {
+                throw new \RuntimeException('Cannot read file');
+            }
+            $output = $chunk . $output;
             // Jump back to where we started reading
             fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
             // Decrease our line counter
@@ -123,13 +125,13 @@ class LogViewer
      */
     public function parse($line)
     {
-        if (!is_string($line) || strlen($line) === 0) {
-            return array();
+        if (!is_string($line) || $line === '') {
+            return [];
         }
 
         preg_match($this->pattern, $line, $data);
         if (!isset($data['date'])) {
-            return array();
+            return [];
         }
 
         preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
@@ -138,7 +140,7 @@ class LogViewer
             $data['trace'] = trim($matches[2]);
         }
 
-        return array(
+        return [
             'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
             'logger' => $data['logger'],
             'level' => $data['level'],
@@ -146,7 +148,7 @@ class LogViewer
             'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
             'context' => json_decode($data['context'], true),
             'extra' => json_decode($data['extra'], true)
-        );
+        ];
     }
 
     /**

+ 1 - 3
system/src/Grav/Common/Helpers/Truncator.php

@@ -230,9 +230,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
     public function filter(callable $callback = null)
     {
         foreach ($this->items as $key => $value) {
-            if ((!$callback && !(bool)$value) ||
-                ($callback && !$callback($value, $key))
-            ) {
+            if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) {
                 unset($this->items[$key]);
             }
         }

+ 2 - 11
system/src/Grav/Common/Language/Language.php

@@ -187,12 +187,7 @@ class LanguageCodes
      */
     public static function getOrientation($code)
     {
-        if (isset(static::$codes[$code])) {
-            if (isset(static::$codes[$code]['orientation'])) {
-                return static::get($code, 'orientation');
-            }
-        }
-        return 'ltr';
+        return static::$codes[$code]['orientation'] ?? 'ltr';
     }
 
     /**
@@ -226,11 +221,7 @@ class LanguageCodes
      */
     public static function get($code, $type)
     {
-        if (isset(static::$codes[$code][$type])) {
-            return static::$codes[$code][$type];
-        }
-
-        return false;
+        return static::$codes[$code][$type] ?? false;
     }
 
     /**

+ 1 - 1
system/src/Grav/Common/Markdown/Parsedown.php

@@ -56,7 +56,7 @@ trait ImageMediaTrait
         'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
         'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
         'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
-        'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format'
+        'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'create', 'fill', 'merge'
     ];
 
     /** @var array */

+ 23 - 17
system/src/Grav/Common/Media/Traits/MediaFileTrait.php

@@ -20,11 +20,13 @@ use Grav\Common\Security;
 use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Form\FormFlashFile;
+use Grav\Framework\Mime\MimeTypes;
 use Psr\Http\Message\UploadedFileInterface;
 use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RuntimeException;
 use function dirname;
+use function in_array;
 
 /**
  * Implements media upload and delete functionality.
@@ -106,7 +108,7 @@ trait MediaUploadTrait
      *
      * @param array $metadata
      * @param array|null $settings
-     * @return string|null
+     * @return string
      * @throws RuntimeException
      */
     public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string
@@ -179,16 +181,20 @@ trait MediaUploadTrait
             }
         }
 
+        $grav = Grav::instance();
+        /** @var MimeTypes $mimeChecker */
+        $mimeChecker = $grav['mime'];
+
         // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
-        $accepted = false;
-        $errors = [];
         // Do not trust mime type sent by the browser.
-        $mime = Utils::getMimeByFilename($filename);
-        $mimeTest = $metadata['mime'] ?? $mime;
-        if ($mime !== $mimeTest) {
+        $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
+        $validExtensions = $mimeChecker->getExtensions($mime);
+        if (!in_array($extension, $validExtensions, true)) {
             throw new RuntimeException('The mime type does not match to file extension', 400);
         }
 
+        $accepted = false;
+        $errors = [];
         foreach ((array)$settings['accept'] as $type) {
             // Force acceptance of any file when star notation
             if ($type === '*') {
@@ -418,6 +424,17 @@ trait MediaUploadTrait
         $uploadedFile->moveTo($filepath);
     }
 
+    /**
+     * Get upload settings.
+     *
+     * @param array|null $settings Form field specific settings (override).
+     * @return array
+     */
+    public function getUploadSettings(?array $settings = null): array
+    {
+        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
+    }
+
     /**
      * Internal logic to copy file.
      *
@@ -604,17 +621,6 @@ trait MediaUploadTrait
         }
     }
 
-    /**
-     * Get upload settings.
-     *
-     * @param array|null $settings Form field specific settings (override).
-     * @return array
-     */
-    protected function getUploadSettings(?array $settings = null): array
-    {
-        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
-    }
-
     /**
      * @param string $filename
      * @param string $path

+ 27 - 13
system/src/Grav/Common/Media/Traits/StaticResizeTrait.php

@@ -47,7 +47,7 @@ class Collection extends Iterator implements PageCollectionInterface
         parent::__construct($items);
 
         $this->params = $params;
-        $this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages');
+        $this->pages = $pages ?: Grav::instance()->offsetGet('pages');
     }
 
     /**
@@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface
         return $this;
     }
 
+    /**
+     * Set current page.
+     */
+    public function setCurrent(string $path): void
+    {
+        reset($this->items);
+
+        while (($key = key($this->items)) !== null && $key !== $path) {
+            next($this->items);
+        }
+    }
+
     /**
      * Returns current page.
      *
@@ -319,30 +331,32 @@ class Collection extends Iterator implements PageCollectionInterface
 
     /**
      * Returns the items between a set of date ranges of either the page date field (default) or
-     * an arbitrary datetime page field where end date is optional
-     * Dates can be passed in as text that strtotime() can process
+     * an arbitrary datetime page field where start date and end date are optional
+     * Dates must be passed in as text that strtotime() can process
      * http://php.net/manual/en/function.strtotime.php
      *
-     * @param string $startDate
-     * @param bool $endDate
+     * @param string|null $startDate
+     * @param string|null $endDate
      * @param string|null $field
      * @return $this
      * @throws Exception
      */
-    public function dateRange($startDate, $endDate = false, $field = null)
+    public function dateRange($startDate = null, $endDate = null, $field = null)
     {
-        $start = Utils::date2timestamp($startDate);
-        $end = $endDate ? Utils::date2timestamp($endDate) : false;
+        $start = $startDate ? Utils::date2timestamp($startDate) : null;
+        $end = $endDate ? Utils::date2timestamp($endDate) : null;
 
         $date_range = [];
         foreach ($this->items as $path => $slug) {
             $page = $this->pages->get($path);
-            if ($page !== null) {
-                $date = $field ? strtotime($page->value($field)) : $page->date();
+            if (!$page) {
+                continue;
+            }
 
-                if ($date >= $start && (!$end || $date <= $end)) {
-                    $date_range[$path] = $slug;
-                }
+            $date = $field ? strtotime($page->value($field)) : $page->date();
+
+            if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
+                $date_range[$path] = $slug;
             }
         }
 

+ 5 - 5
system/src/Grav/Common/Page/Header.php

@@ -158,17 +158,17 @@ interface PageCollectionInterface extends Traversable, ArrayAccess, Countable, S
 
     /**
      * Returns the items between a set of date ranges of either the page date field (default) or
-     * an arbitrary datetime page field where end date is optional
-     * Dates can be passed in as text that strtotime() can process
+     * an arbitrary datetime page field where start date and end date are optional
+     * Dates must be passed in as text that strtotime() can process
      * http://php.net/manual/en/function.strtotime.php
      *
-     * @param string $startDate
-     * @param bool $endDate
+     * @param string|null $startDate
+     * @param string|null $endDate
      * @param string|null $field
      * @return PageCollectionInterface
      * @throws Exception
      */
-    public function dateRange($startDate, $endDate = false, $field = null);
+    public function dateRange($startDate = null, $endDate = null, $field = null);
 
     /**
      * Creates new collection with only visible pages

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

@@ -157,7 +157,7 @@ class Excerpts
             // Handle custom streams.
             /** @var UniformResourceLocator $locator */
             $locator = $grav['locator'];
-            if ($locator->isStream($url)) {
+            if ($type === 'link' && $locator->isStream($url)) {
                 $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
                 $url_parts['path'] = $grav['base_url_relative'] . '/' . $path;
                 unset($url_parts['stream'], $url_parts['scheme']);

+ 3 - 2
system/src/Grav/Common/Page/Media.php

@@ -102,12 +102,13 @@ class Media extends AbstractMedia
 
         foreach ($iterator as $file => $info) {
             // Ignore folders and Markdown files.
-            if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
+            $filename = $info->getFilename();
+            if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
                 continue;
             }
 
             // Find out what type we're dealing with
-            [$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
+            [$basename, $ext, $type, $extra] = $this->getFileParts($filename);
 
             if (!in_array(strtolower($ext), $media_types, true)) {
                 continue;

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

@@ -337,6 +337,37 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
         return $this;
     }
 
+    /**
+     * Add a frame to image
+     *
+     * @return $this
+     */
+    public function addFrame(int $border = 10, string $color = '0x000000')
+    {
+      if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
+        $image = ImageFile::open($this->path());
+      }
+      else {
+        return $this;
+      }
+
+      $dst_width = $image->width()+2*$border;
+      $dst_height = $image->height()+2*$border;
+
+      $frame = ImageFile::create($dst_width, $dst_height);
+
+      $frame->__call('fill', [$color]);
+
+      $this->image = $frame;
+
+      $this->__call('merge', [$image, $border, $border]);
+
+      $this->saveImage();
+
+      return $this;
+
+    }
+
     /**
      * Forward the call to the image processing method.
      *
@@ -344,6 +375,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
      * @param mixed $args
      * @return $this|mixed
      */
+    
     public function __call($method, $args)
     {
         if (!in_array($method, static::$magic_actions, true)) {

+ 10 - 7
system/src/Grav/Common/Page/Medium/Link.php

@@ -41,9 +41,7 @@ class Link implements RenderableInterface, MediaLinkInterface
         $this->attributes = $attributes;
 
         $source = $medium->reset()->thumbnail('auto')->display('thumbnail');
-
-        // FIXME: Thumbnail can be null, maybe we should not allow that?
-        if (null === $source) {
+        if (!$source instanceof MediaObjectInterface) {
             throw new RuntimeException('Media has no thumbnail set');
         }
 
@@ -89,10 +87,15 @@ class Link implements RenderableInterface, MediaLinkInterface
             throw new BadMethodCallException(get_class($object) . '::' . $method . '() not found.');
         }
 
-        $this->source = call_user_func_array($callable, $args);
+        $object = call_user_func_array($callable, $args);
+        if (!$object instanceof MediaLinkInterface) {
+            // Don't start nesting links, if user has multiple link calls in his
+            // actions, we will drop the previous links.
+            return $this;
+        }
+
+        $this->source = $object;
 
-        // Don't start nesting links, if user has multiple link calls in his
-        // actions, we will drop the previous links.
-        return $this->source instanceof MediaLinkInterface ? $this->source : $this;
+        return $object;
     }
 }

+ 2 - 2
system/src/Grav/Common/Page/Medium/Medium.php

@@ -2274,11 +2274,11 @@ class Page implements PageInterface
     {
         if ($var !== null) {
             // make sure first level are arrays
-            array_walk($var, function (&$value) {
+            array_walk($var, static function (&$value) {
                 $value = (array) $value;
             });
             // make sure all values are strings
-            array_walk_recursive($var, function (&$value) {
+            array_walk_recursive($var, static function (&$value) {
                 $value = (string) $value;
             });
             $this->taxonomy = $var;

+ 76 - 14
system/src/Grav/Common/Page/Pages.php

@@ -196,6 +196,58 @@ class Pages
         return $this->baseRoute($lang) . $route;
     }
 
+    /**
+     * Get relative referrer route and language code. Returns null if the route isn't within the current base, language (if set) and route.
+     *
+     * @example `$langCode = null; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within /admin and updates $langCode
+     * @example `$langCode = 'en'; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within the /en/admin
+     *
+     * @param string|null $langCode Variable to store the language code. If already set, check only against that language.
+     * @param string $route Optional route within the site.
+     * @return string|null
+     * @since 1.7.23
+     */
+    public function referrerRoute(?string &$langCode, string $route = '/'): ?string
+    {
+        $referrer = $_SERVER['HTTP_REFERER'] ?? null;
+
+        // Start by checking that referrer came from our site.
+        $root = $this->grav['base_url_absolute'];
+        if (!is_string($referrer) || !str_starts_with($referrer, $root)) {
+            return null;
+        }
+
+        /** @var Language $language */
+        $language = $this->grav['language'];
+
+        // Get all language codes and append no language.
+        if (null === $langCode) {
+            $languages = $language->enabled() ? $language->getLanguages() : [];
+            $languages[] = '';
+        } else {
+            $languages[] = $langCode;
+        }
+
+        $path_base = rtrim($this->base(), '/');
+        $path_route = rtrim($route, '/');
+
+        // Try to figure out the language code.
+        foreach ($languages as $code) {
+            $path_lang = $code ? "/{$code}" : '';
+
+            $base = $path_base . $path_lang . $path_route;
+            if ($referrer === $base || str_starts_with($referrer, "{$base}/")) {
+                if (null === $langCode) {
+                    $langCode = $code;
+                }
+
+                return substr($referrer, \strlen($base));
+            }
+        }
+
+        return null;
+    }
+
     /**
      *
      * Get base URL for Grav pages.
@@ -274,7 +326,7 @@ class Pages
      *
      * @return void
      */
-    public function reset()
+    public function reset(): void
     {
         $this->initialized = false;
 
@@ -540,9 +592,9 @@ class Pages
         }
 
         if (isset($params['dateRange'])) {
-            $start = $params['dateRange']['start'] ?? 0;
-            $end = $params['dateRange']['end'] ?? false;
-            $field = $params['dateRange']['field'] ?? false;
+            $start = $params['dateRange']['start'] ?? null;
+            $end = $params['dateRange']['end'] ?? null;
+            $field = $params['dateRange']['field'] ?? null;
             $collection = $collection->dateRange($start, $end, $field);
         }
 
@@ -554,7 +606,7 @@ class Pages
 
             if (is_array($sort_flags)) {
                 $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
-                $sort_flags = array_reduce($sort_flags, function ($a, $b) {
+                $sort_flags = array_reduce($sort_flags, static function ($a, $b) {
                     return $a | $b;
                 }, 0); //merge constant values using bit or
             }
@@ -663,29 +715,39 @@ class Pages
 
         switch ($type) {
             case 'all':
-                return $page->children();
+                $collection = $page->children();
+                break;
             case 'modules':
             case 'modular':
-                return $page->children()->modules();
+                $collection = $page->children()->modules();
+                break;
             case 'pages':
             case 'children':
-                return $page->children()->pages();
+                $collection = $page->children()->pages();
+                break;
             case 'page':
             case 'self':
-                return !$page->root() ? (new Collection())->addPage($page) : new Collection();
+                $collection = !$page->root() ? (new Collection())->addPage($page) : new Collection();
+                break;
             case 'parent':
                 $parent = $page->parent();
                 $collection = new Collection();
-                return $parent ? $collection->addPage($parent) : $collection;
+                $collection = $parent ? $collection->addPage($parent) : $collection;
+                break;
             case 'siblings':
                 $parent = $page->parent();
-                return $parent ? $parent->children()->remove($page->path()) : new Collection();
+                $collection = $parent ? $parent->children()->remove($page->path()) : new Collection();
+                break;
             case 'descendants':
-                return $this->all($page)->remove($page->path())->pages();
+                $collection = $this->all($page)->remove($page->path())->pages();
+                break;
             default:
                 // Unknown type; return empty collection.
-                return new Collection();
+                $collection = new Collection();
+                break;
         }
+
+        return $collection;
     }
 
     /**
@@ -1761,7 +1823,7 @@ class Pages
         // Build regular expression for all the allowed page extensions.
         $page_extensions = $language->getFallbackPageExtensions();
         $regex = '/^[^\.]*(' . implode('|', array_map(
-            function ($str) {
+            static function ($str) {
                 return preg_quote($str, '/');
             },
             $page_extensions

+ 21 - 0
system/src/Grav/Common/Page/Traits/PageFormTrait.php

@@ -10,6 +10,7 @@
 namespace Grav\Common;
 
 use ArrayAccess;
+use Composer\Autoload\ClassLoader;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\Data;
 use Grav\Common\Page\Interfaces\PageInterface;
@@ -42,6 +43,8 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
     protected $active = true;
     /** @var Blueprint|null */
     protected $blueprint;
+    /** @var ClassLoader|null */
+    protected $loader;
 
     /**
      * By default assign all methods as listeners using the default priority.
@@ -79,6 +82,24 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
         }
     }
 
+    /**
+     * @return ClassLoader|null
+     * @internal
+     */
+    final public function getAutoloader(): ?ClassLoader
+    {
+        return $this->loader;
+    }
+
+    /**
+     * @param ClassLoader|null $loader
+     * @internal
+     */
+    final public function setAutoloader(?ClassLoader $loader): void
+    {
+        $this->loader = $loader;
+    }
+
     /**
      * @param Config $config
      * @return $this

+ 26 - 17
system/src/Grav/Common/Plugins.php

@@ -143,7 +143,7 @@ class Plugins extends Iterator
                 $instance->setConfig($config);
                 // Register autoloader.
                 if (method_exists($instance, 'autoload')) {
-                    $instance->autoload();
+                    $instance->setAutoloader($instance->autoload());
                 }
                 // Register event listeners.
                 $events->addSubscriber($instance);
@@ -287,33 +287,42 @@ class Plugins extends Iterator
     {
         // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM!
         $grav = Grav::instance();
+        /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
+        $class = null;
 
+        // Start by attempting to load the plugin_name.php file.
+        $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
         if (is_file($file)) {
             // Local variables available in the file: $grav, $name, $file
             $class = include_once $file;
+            if (!is_object($class) || !is_subclass_of($class, Plugin::class, true)) {
+                $class = null;
+            }
+        }
 
-            if (!$class || !is_subclass_of($class, Plugin::class, true)) {
-                $className = Inflector::camelize($name);
-                $pluginClassFormat = [
-                    'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
-                    'Grav\\Plugin\\' . $className . 'Plugin',
-                    'Grav\\Plugin\\' . $className
-                ];
-
-                foreach ($pluginClassFormat as $pluginClass) {
-                    if (is_subclass_of($pluginClass, Plugin::class, true)) {
-                        $class = new $pluginClass($name, $grav);
-                        break;
-                    }
+        // If the class hasn't been initialized yet, guess the class name and create a new instance.
+        if (null === $class) {
+            $className = Inflector::camelize($name);
+            $pluginClassFormat = [
+                'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
+                'Grav\\Plugin\\' . $className . 'Plugin',
+                'Grav\\Plugin\\' . $className
+            ];
+
+            foreach ($pluginClassFormat as $pluginClass) {
+                if (is_subclass_of($pluginClass, Plugin::class, true)) {
+                    $class = new $pluginClass($name, $grav);
+                    break;
                 }
             }
-        } else {
+        }
+
+        // Log a warning if plugin cannot be found.
+        if (null === $class) {
             $grav['log']->addWarning(
                 sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
             );
-            return null;
         }
 
         return $class;

+ 3 - 3
system/src/Grav/Common/Processors/AssetsProcessor.php

@@ -105,12 +105,12 @@ class InitializeProcessor extends ProcessorBase
         // TODO: remove in 2.0.
         $this->container['accounts'];
 
-        // Initialize session.
-        $this->initializeSession($config);
-
         // Initialize URI (uses session, see issue #3269).
         $this->initializeUri($config);
 
+        // Initialize session.
+        $this->initializeSession($config);
+
         // Grav may return redirect response right away.
         $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
         if ($redirectCode) {

+ 20 - 3
system/src/Grav/Common/Processors/PagesProcessor.php

@@ -10,6 +10,8 @@
 namespace Grav\Common\Processors;
 
 use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Form\Forms;
 use RocketTheme\Toolbox\Event\Event;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
@@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
         $page = $this->container['page'];
 
         if (!$page->routable()) {
+            $exception = new RequestException($request, 'Page Not Found', 404);
+            $route = $this->container['route'];
             // If no page found, fire event
-            $event = new Event(['page' => $page]);
+            $event = new Event([
+                'page' => $page,
+                'code' => $exception->getCode(),
+                'message' => $exception->getMessage(),
+                'exception' => $exception,
+                'route' => $route,
+                'request' => $request
+            ]);
             $event->page = null;
             $event = $this->container->fireEvent('onPageNotFound', $event);
 
@@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase
 
             $task = $this->container['task'];
             $action = $this->container['action'];
+
+            /** @var Forms $forms */
+            $forms = $this->container['forms'] ?? null;
+            $form = $forms ? $forms->getActiveForm() : null;
+
+            $options = ['page' => $page, 'form' => $form, 'request' => $request];
             if ($task) {
-                $event = new Event(['task' => $task, 'page' => $page]);
+                $event = new Event(['task' => $task] + $options);
                 $this->container->fireEvent('onPageTask', $event);
                 $this->container->fireEvent('onPageTask.' . $task, $event);
             } elseif ($action) {
-                $event = new Event(['action' => $action, 'page' => $page]);
+                $event = new Event(['action' => $action] + $options);
                 $this->container->fireEvent('onPageAction', $event);
                 $this->container->fireEvent('onPageAction.' . $action, $event);
             }

+ 12 - 7
system/src/Grav/Common/Processors/PluginsProcessor.php

@@ -14,6 +14,7 @@ use Grav\Framework\Psr7\Response;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use RocketTheme\Toolbox\Event\Event;
 
 /**
  * Class RenderProcessor
@@ -42,23 +43,27 @@ class RenderProcessor extends ProcessorBase
             return $output;
         }
 
-        ob_start();
+        /** @var PageInterface $page */
+        $page = $this->container['page'];
 
         // Use internal Grav output.
         $container->output = $output;
-        $container->fireEvent('onOutputGenerated');
+
+        ob_start();
+
+        $event = new Event(['page' => $page, 'output' => &$container->output]);
+        $container->fireEvent('onOutputGenerated', $event);
 
         echo $container->output;
 
+        $html = ob_get_clean();
+
         // remove any output
         $container->output = '';
 
-        $this->container->fireEvent('onOutputRendered');
-
-        $html = ob_get_clean();
+        $event = new Event(['page' => $page, 'output' => $html]);
+        $this->container->fireEvent('onOutputRendered', $event);
 
-        /** @var PageInterface $page */
-        $page = $this->container['page'];
         $this->stopTimer();
 
         return new Response($page->httpResponseCode(), $page->httpHeaders(), $html);

+ 4 - 2
system/src/Grav/Common/Processors/RequestProcessor.php

@@ -271,7 +271,7 @@ class Job
         if ($whenOverlapping) {
             $this->whenOverlapping = $whenOverlapping;
         } else {
-            $this->whenOverlapping = function () {
+            $this->whenOverlapping = static function () {
                 return false;
             };
         }
@@ -390,7 +390,9 @@ class Job
         if (count($this->outputTo) > 0) {
             foreach ($this->outputTo as $file) {
                 $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
-                file_put_contents($file, $this->output, $output_mode);
+                $timestamp = (new DateTime('now'))->format('c');
+                $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
+                file_put_contents($file, $output, $output_mode);
             }
         }
 

+ 16 - 8
system/src/Grav/Common/Scheduler/Scheduler.php

@@ -9,10 +9,11 @@
 
 namespace Grav\Common;
 
-use enshrined\svgSanitize\Sanitizer;
 use Exception;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Page\Pages;
+use Rhukster\DomSanitizer\DOMSanitizer;
 use function chr;
 use function count;
 use function is_array;
@@ -33,7 +34,7 @@ class Security
     public static function sanitizeSvgString(string $svg): string
     {
         if (Grav::instance()['config']->get('security.sanitize_svg')) {
-            $sanitizer = new Sanitizer();
+            $sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
             $sanitized = $sanitizer->sanitize($svg);
             if (is_string($sanitized)) {
                 $svg = $sanitized;
@@ -52,13 +53,20 @@ class Security
     public static function sanitizeSVG(string $file): void
     {
         if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) {
-            $sanitizer = new Sanitizer();
+            $sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
             $original_svg = file_get_contents($file);
             $clean_svg = $sanitizer->sanitize($original_svg);
 
-            // TODO: what to do with bad SVG files which return false?
-            if ($clean_svg !== false && $clean_svg !== $original_svg) {
+            // Quarantine bad SVG files and throw exception
+            if ($clean_svg !== false ) {
                 file_put_contents($file, $clean_svg);
+            } else {
+                $quarantine_file = basename($file);
+                $quarantine_dir = 'log://quarantine';
+                Folder::mkdir($quarantine_dir);
+                file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
+                unlink($file);
+                throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
             }
         }
     }
@@ -99,7 +107,7 @@ class Security
                 $content = $page->value('content');
 
                 $data = ['header' => $header, 'content' => $content];
-                $results = Security::detectXssFromArray($data);
+                $results = static::detectXssFromArray($data);
 
                 if (!empty($results)) {
                     if ($route) {
@@ -191,7 +199,7 @@ class Security
         $string = urldecode($string);
 
         // Convert Hexadecimals
-        $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function ($m) {
+        $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) {
             return chr(hexdec($m[2]));
         }, $string);
 
@@ -231,7 +239,7 @@ class Security
             }
         }
 
-        return false;
+        return null;
     }
 
     public static function getXssDefaults(): array

+ 14 - 0
system/src/Grav/Common/Service/AccountsServiceProvider.php

@@ -17,6 +17,7 @@ use Grav\Common\Config\Config;
 use Grav\Common\Config\ConfigFileFinder;
 use Grav\Common\Config\Setup;
 use Grav\Common\Language\Language;
+use Grav\Framework\Mime\MimeTypes;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
 use RocketTheme\Toolbox\File\YamlFile;
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
             return $config;
         };
 
+        $container['mime'] = function ($c) {
+            /** @var Config $config */
+            $config = $c['config'];
+            $mimes = $config->get('mime.types', []);
+            foreach ($config->get('media.types', []) as $ext => $media) {
+                if (!empty($media['mime'])) {
+                    $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
+                }
+            }
+
+            return MimeTypes::createFromMimes($mimes);
+        };
+
         $container['languages'] = function ($c) {
             return static::languages($c);
         };

+ 11 - 2
system/src/Grav/Common/Service/ErrorServiceProvider.php

@@ -12,6 +12,7 @@ namespace Grav\Common\Service;
 use Grav\Common\Grav;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Class TaskServiceProvider
@@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
     public function register(Container $container)
     {
         $container['task'] = function (Grav $c) {
-            $task = $_POST['task'] ?? $c['uri']->param('task');
+            /** @var ServerRequestInterface $request */
+            $request = $c['request'];
+            $body = $request->getParsedBody();
+
+            $task = $body['task'] ?? $c['uri']->param('task');
             if (null !== $task) {
                 $task = filter_var($task, FILTER_SANITIZE_STRING);
             }
@@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
         };
 
         $container['action'] = function (Grav $c) {
-            $action = $_POST['action'] ?? $c['uri']->param('action');
+            /** @var ServerRequestInterface $request */
+            $request = $c['request'];
+            $body = $request->getParsedBody();
+
+            $action = $body['action'] ?? $c['uri']->param('action');
             if (null !== $action) {
                 $action = filter_var($action, FILTER_SANITIZE_STRING);
             }

+ 10 - 6
system/src/Grav/Common/Session.php

@@ -12,6 +12,7 @@ namespace Grav\Common;
 use Grav\Common\Form\FormFlash;
 use Grav\Events\SessionStartEvent;
 use Grav\Plugin\Form\Forms;
+use JsonException;
 use function is_string;
 
 /**
@@ -128,12 +129,12 @@ class Session extends \Grav\Framework\Session\Session
                 /** @var Uri $uri */
                 $uri = $grav['uri'];
                 /** @var Forms|null $form */
-                $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line
+                $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line (form plugin)
 
                 $sessionField = base64_encode($uri->url);
 
                 /** @var FormFlash|null $flash */
-                $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line
+                $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line (form plugin)
                 $object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null;
             }
         }
@@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
      * @param mixed $object
      * @param int $time
      * @return $this
+     * @throws JsonException
      */
     public function setFlashCookieObject($name, $object, $time = 60)
     {
-        setcookie($name, json_encode($object), time() + $time, '/');
+        setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
 
         return $this;
     }
@@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
      *
      * @param string $name
      * @return mixed|null
+     * @throws JsonException
      */
     public function getFlashCookieObject($name)
     {
         if (isset($_COOKIE[$name])) {
-            $object = json_decode($_COOKIE[$name], false);
-            setcookie($name, '', time() - 3600, '/');
-            return $object;
+            $cookie = $_COOKIE[$name];
+            setcookie($name, '', $this->getCookieOptions(-42000));
+
+            return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
         }
 
         return null;

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

@@ -105,7 +105,7 @@ class Taxonomy
             }
         } elseif (is_string($value)) {
             if (!empty($key)) {
-                $taxonomy = $taxonomy . $key;
+                $taxonomy .= $key;
             }
             $this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()];
         }

+ 23 - 17
system/src/Grav/Common/Theme.php

@@ -224,28 +224,18 @@ class Themes extends Iterator
         $grav = $this->grav;
         $config = $this->config;
         $name = $this->current();
+        $class = null;
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
 
+        // Start by attempting to load the theme.php file.
+        $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
         if ($file) {
             // Local variables available in the file: $grav, $config, $name, $file
             $class = include $file;
-
-            if (!$class || !is_subclass_of($class, Plugin::class, true)) {
-                $className = Inflector::camelize($name);
-                $themeClassFormat = [
-                    'Grav\\Theme\\' . $className,
-                    'Grav\\Theme\\' . ucfirst($name)
-                ];
-
-                foreach ($themeClassFormat as $themeClass) {
-                    if (is_subclass_of($themeClass, Theme::class, true)) {
-                        $class = new $themeClass($grav, $config, $name);
-                        break;
-                    }
-                }
+            if (!\is_object($class) || !is_subclass_of($class, Theme::class, true)) {
+                $class = null;
             }
         } elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
             $response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
@@ -253,12 +243,28 @@ class Themes extends Iterator
             $grav->close($response);
         }
 
-        $this->config->set('theme', $config->get('themes.' . $name));
+        // If the class hasn't been initialized yet, guess the class name and create a new instance.
+        if (null === $class) {
+            $themeClassFormat = [
+                'Grav\\Theme\\' . Inflector::camelize($name),
+                'Grav\\Theme\\' . ucfirst($name)
+            ];
+
+            foreach ($themeClassFormat as $themeClass) {
+                if (is_subclass_of($themeClass, Theme::class, true)) {
+                    $class = new $themeClass($grav, $config, $name);
+                    break;
+                }
+            }
+        }
 
-        if (empty($class)) {
+        // Finally if everything else fails, just create a new instance from the default Theme class.
+        if (null === $class) {
             $class = new Theme($grav, $config, $name);
         }
 
+        $this->config->set('theme', $config->get('themes.' . $name));
+
         return $class;
     }
 

+ 19 - 0
system/src/Grav/Common/Twig/Exception/TwigException.php

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @package    Grav\Common\Twig\Exception
+ *
+ * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Twig\Exception;
+
+/**
+ * TwigException gets thrown when you use {% throw code message %} in twig.
+ *
+ * This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
+ */
+class TwigException extends \RuntimeException
+{
+}

+ 42 - 2
system/src/Grav/Common/Twig/Extension/FilesystemExtension.php

@@ -13,6 +13,7 @@ use Grav\Common\Grav;
 use Grav\Common\Utils;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
 use Twig\TwigFunction;
 
 /**
@@ -30,11 +31,35 @@ class FilesystemExtension extends AbstractExtension
     }
 
     /**
-     * @return TwigFunction[]
+     * @return TwigFilter[]
      */
     public function getFilters()
     {
-        return $this->getFunctions();
+        return [
+            new TwigFilter('file_exists', [$this, 'file_exists']),
+            new TwigFilter('fileatime', [$this, 'fileatime']),
+            new TwigFilter('filectime', [$this, 'filectime']),
+            new TwigFilter('filemtime', [$this, 'filemtime']),
+            new TwigFilter('filesize', [$this, 'filesize']),
+            new TwigFilter('filetype', [$this, 'filetype']),
+            new TwigFilter('is_dir', [$this, 'is_dir']),
+            new TwigFilter('is_file', [$this, 'is_file']),
+            new TwigFilter('is_link', [$this, 'is_link']),
+            new TwigFilter('is_readable', [$this, 'is_readable']),
+            new TwigFilter('is_writable', [$this, 'is_writable']),
+            new TwigFilter('is_writeable', [$this, 'is_writable']),
+            new TwigFilter('lstat', [$this, 'lstat']),
+            new TwigFilter('getimagesize', [$this, 'getimagesize']),
+            new TwigFilter('exif_read_data', [$this, 'exif_read_data']),
+            new TwigFilter('read_exif_data', [$this, 'exif_read_data']),
+            new TwigFilter('exif_imagetype', [$this, 'exif_imagetype']),
+            new TwigFilter('hash_file', [$this, 'hash_file']),
+            new TwigFilter('hash_hmac_file', [$this, 'hash_hmac_file']),
+            new TwigFilter('md5_file', [$this, 'md5_file']),
+            new TwigFilter('sha1_file', [$this, 'sha1_file']),
+            new TwigFilter('get_meta_tags', [$this, 'get_meta_tags']),
+            new TwigFilter('pathinfo', [$this, 'pathinfo']),
+        ];
     }
 
     /**
@@ -67,6 +92,7 @@ class FilesystemExtension extends AbstractExtension
             new TwigFunction('md5_file', [$this, 'md5_file']),
             new TwigFunction('sha1_file', [$this, 'sha1_file']),
             new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']),
+            new TwigFunction('pathinfo', [$this, 'pathinfo']),
         ];
     }
 
@@ -340,6 +366,20 @@ class FilesystemExtension extends AbstractExtension
         return get_meta_tags($filename);
     }
 
+    /**
+     * @param string $path
+     * @param int|null $flags
+     * @return string|string[]
+     */
+    public function pathinfo($path, $flags = null)
+    {
+        if (null !== $flags) {
+            return pathinfo($path, (int)$flags);
+        }
+
+        return pathinfo($path);
+    }
+
     /**
      * @param string $filename
      * @return bool

+ 51 - 13
system/src/Grav/Common/Twig/Extension/GravExtension.php

@@ -90,7 +90,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
      *
      * @return array
      */
-    public function getGlobals()
+    public function getGlobals(): array
     {
         return [
             'grav' => $this->grav,
@@ -102,7 +102,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
      *
      * @return array
      */
-    public function getFilters()
+    public function getFilters(): array
     {
         return [
             new TwigFilter('*ize', [$this, 'inflectorFilter']),
@@ -155,10 +155,15 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             new TwigFilter('bool', [$this, 'boolFilter']),
             new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
             new TwigFilter('array', [$this, 'arrayFilter']),
+            new TwigFilter('yaml', [$this, 'yamlFilter']),
 
             // Object Types
             new TwigFilter('get_type', [$this, 'getTypeFunc']),
-            new TwigFilter('of_type', [$this, 'ofTypeFunc'])
+            new TwigFilter('of_type', [$this, 'ofTypeFunc']),
+
+            // PHP methods
+            new TwigFilter('count', 'count'),
+            new TwigFilter('array_diff', 'array_diff'),
         ];
     }
 
@@ -167,7 +172,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
      *
      * @return array
      */
-    public function getFunctions()
+    public function getFunctions(): array
     {
         return [
             new TwigFunction('array', [$this, 'arrayFilter']),
@@ -212,6 +217,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             new TwigFunction('cron', [$this, 'cronFunc']),
             new TwigFunction('svg_image', [$this, 'svgImageFunction']),
             new TwigFunction('xss', [$this, 'xssFunc']),
+            new TwigFunction('unique_id', [$this, 'uniqueId']),
 
             // Translations
             new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
@@ -220,14 +226,25 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
 
             // Object Types
             new TwigFunction('get_type', [$this, 'getTypeFunc']),
-            new TwigFunction('of_type', [$this, 'ofTypeFunc'])
+            new TwigFunction('of_type', [$this, 'ofTypeFunc']),
+
+            // PHP methods
+            new TwigFunction('is_numeric', 'is_numeric'),
+            new TwigFunction('is_iterable', 'is_iterable'),
+            new TwigFunction('is_countable', 'is_countable'),
+            new TwigFunction('is_null', 'is_null'),
+            new TwigFunction('is_string', 'is_string'),
+            new TwigFunction('is_array', 'is_array'),
+            new TwigFunction('is_object', 'is_object'),
+            new TwigFunction('count', 'count'),
+            new TwigFunction('array_diff', 'array_diff'),
         ];
     }
 
     /**
      * @return array
      */
-    public function getTokenParsers()
+    public function getTokenParsers(): array
     {
         return [
             new TwigTokenParserRender(),
@@ -638,6 +655,20 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
         return implode(', ', $results_parts);
     }
 
+    /**
+     * Generates a random string with configurable length, prefix and suffix.
+     * Unlike the built-in `uniqid()`, this string is non-conflicting and safe
+     *
+     * @param int $length
+     * @param array $options
+     * @return string
+     * @throws \Exception
+     */
+    public function uniqueId(int $length = 9, array $options = ['prefix' => '', 'suffix' => '']): string
+    {
+        return Utils::uniqueId($length, $options);
+    }
+
     /**
      * @param string $string
      * @return string
@@ -793,15 +824,22 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
     }
 
     /**
-     * @param Environment $twig
+     * @param array|object $value
+     * @param int|null $inline
+     * @param int|null $indent
      * @return string
      */
-    public function translate(Environment $twig)
+    public function yamlFilter($value, $inline = null, $indent = null): string
     {
-        // shift off the environment
-        $args = func_get_args();
-        array_shift($args);
+        return Yaml::dump($value, $inline, $indent);
+    }
 
+    /**
+     * @param Environment $twig
+     * @return string
+     */
+    public function translate(Environment $twig, ...$args)
+    {
         // If admin and tu filter provided, use it
         if (isset($this->grav['admin'])) {
             $numargs = count($args);
@@ -1484,7 +1522,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             }
 
             //Look for existing class
-            $svg = preg_replace_callback('/^<svg[^>]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) {
+            $svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
                 if (isset($matches[2])) {
                     $new_classes = $matches[2] . $classes;
                     $matched = true;
@@ -1500,7 +1538,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
                 $svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
             }
 
-            return $svg;
+            return trim($svg);
         }
 
         return null;

+ 1 - 1
system/src/Grav/Common/Twig/Node/TwigNodeCache.php

@@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
         $compiler->addDebugInfo($this);
 
         $compiler
-            ->write('throw new \RuntimeException(')
+            ->write('throw new \Grav\Common\Twig\Exception\TwigException(')
             ->subcompile($this->getNode('message'))
             ->write(', ')
             ->write($this->getAttribute('code') ?: 500)

+ 7 - 8
system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php

@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
 
         $compiler
             ->indent()
-            ->subcompile($this->getNode('try'));
+            ->subcompile($this->getNode('try'))
+            ->outdent()
+            ->write('} catch (\Exception $e) {' . "\n")
+            ->indent()
+            ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
+            ->write('$context[\'e\'] = $e;' . "\n");
 
         if ($this->hasNode('catch')) {
-            $compiler
-                ->outdent()
-                ->write('} catch (\Exception $e) {' . "\n")
-                ->indent()
-                ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
-                ->write('$context[\'e\'] = $e;' . "\n")
-                ->subcompile($this->getNode('catch'));
+            $compiler->subcompile($this->getNode('catch'));
         }
 
         $compiler

+ 68 - 46
system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php

@@ -16,22 +16,23 @@ use Grav\Common\Language\Language;
 use Grav\Common\Language\LanguageCodes;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Pages;
+use Grav\Common\Twig\Exception\TwigException;
 use Grav\Common\Twig\Extension\FilesystemExtension;
 use Grav\Common\Twig\Extension\GravExtension;
 use Grav\Common\Utils;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\Event\Event;
-use Phive\Twig\Extensions\Deferred\DeferredExtension;
 use RuntimeException;
 use Twig\Cache\FilesystemCache;
+use Twig\DeferredExtension\DeferredExtension;
 use Twig\Environment;
 use Twig\Error\LoaderError;
+use Twig\Error\RuntimeError;
 use Twig\Extension\CoreExtension;
 use Twig\Extension\DebugExtension;
 use Twig\Extension\StringLoaderExtension;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\ChainLoader;
-use Twig\Loader\ExistsLoaderInterface;
 use Twig\Loader\FilesystemLoader;
 use Twig\Profiler\Profile;
 use Twig\TwigFilter;
@@ -404,38 +405,63 @@ class Twig
      */
     public function processSite($format = null, array $vars = [])
     {
-        // set the page now its been processed
-        $this->grav->fireEvent('onTwigSiteVariables');
-        /** @var Pages $pages */
-        $pages = $this->grav['pages'];
-        /** @var PageInterface $page */
-        $page = $this->grav['page'];
-        $content = $page->content();
+        try {
+            $grav = $this->grav;
 
-        $twig_vars = $this->twig_vars;
+            // set the page now its been processed
+            $grav->fireEvent('onTwigSiteVariables');
 
-        $twig_vars['theme'] = $this->grav['config']->get('theme');
-        $twig_vars['pages'] = $pages->root();
-        $twig_vars['page'] = $page;
-        $twig_vars['header'] = $page->header();
-        $twig_vars['media'] = $page->media();
-        $twig_vars['content'] = $content;
-
-        // determine if params are set, if so disable twig cache
-        $params = $this->grav['uri']->params(null, true);
-        if (!empty($params)) {
-            $this->twig->setCache(false);
-        }
+            /** @var Pages $pages */
+            $pages = $grav['pages'];
+
+            /** @var PageInterface $page */
+            $page = $grav['page'];
+
+            $twig_vars = $this->twig_vars;
+            $twig_vars['theme'] = $grav['config']->get('theme');
+            $twig_vars['pages'] = $pages->root();
+            $twig_vars['page'] = $page;
+            $twig_vars['header'] = $page->header();
+            $twig_vars['media'] = $page->media();
+            $twig_vars['content'] = $page->content();
+
+            // determine if params are set, if so disable twig cache
+            $params = $grav['uri']->params(null, true);
+            if (!empty($params)) {
+                $this->twig->setCache(false);
+            }
 
-        // Get Twig template layout
-        $template = $this->getPageTwigTemplate($page, $format);
-        $page->templateFormat($format);
+            // Get Twig template layout
+            $template = $this->getPageTwigTemplate($page, $format);
+            $page->templateFormat($format);
 
-        try {
             $output = $this->twig->render($template, $vars + $twig_vars);
         } catch (LoaderError $e) {
-            $error_msg = $e->getMessage();
-            throw new RuntimeException($error_msg, 400, $e);
+            throw new RuntimeException($e->getMessage(), 400, $e);
+        } catch (RuntimeError $e) {
+            $prev = $e->getPrevious();
+            if ($prev instanceof TwigException) {
+                $code = $prev->getCode() ?: 500;
+                // Fire onPageNotFound event.
+                $event = new Event([
+                    'page' => $page,
+                    'code' => $code,
+                    'message' => $prev->getMessage(),
+                    'exception' => $prev,
+                    'route' => $grav['route'],
+                    'request' => $grav['request']
+                ]);
+                $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
+                $newPage = $event['page'];
+                if ($newPage && $newPage !== $page) {
+                    unset($grav['page']);
+                    $grav['page'] = $newPage;
+
+                    return $this->processSite($newPage->templateFormat(), $vars);
+                }
+            }
+
+            throw $e;
         }
 
         return $output;
@@ -488,25 +514,21 @@ class Twig
         $twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT;
         $template_file = $this->template($page->template() . $twig_extension);
 
-        $page_template = null;
-
         $loader = $this->twig->getLoader();
-        if ($loader instanceof ExistsLoaderInterface) {
-            if ($loader->exists($template_file)) {
-                // template.xxx.twig
-                $page_template = $template_file;
-            } elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) {
-                // template.html.twig
-                $page_template = $template . TEMPLATE_EXT;
-                $format = 'html';
-            } elseif ($loader->exists($default . $twig_extension)) {
-                // default.xxx.twig
-                $page_template = $default . $twig_extension;
-            } else {
-                // default.html.twig
-                $page_template = $default . TEMPLATE_EXT;
-                $format = 'html';
-            }
+        if ($loader->exists($template_file)) {
+            // template.xxx.twig
+            $page_template = $template_file;
+        } elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) {
+            // template.html.twig
+            $page_template = $template . TEMPLATE_EXT;
+            $format = 'html';
+        } elseif ($loader->exists($default . $twig_extension)) {
+            // default.xxx.twig
+            $page_template = $default . $twig_extension;
+        } else {
+            // default.html.twig
+            $page_template = $default . TEMPLATE_EXT;
+            $format = 'html';
         }
 
         return $page_template;

+ 37 - 30
system/src/Grav/Common/Twig/TwigClockworkDataSource.php

@@ -160,8 +160,8 @@ class Uri
         $language = $grav['language'];
 
         // add the port to the base for non-standard ports
-        if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) {
-            $this->base .= ':' . (string)$this->port;
+        if ($this->port && $config->get('system.reverse_proxy_setup') === false) {
+            $this->base .= ':' . $this->port;
         }
 
         // Handle custom base
@@ -176,8 +176,8 @@ class Uri
             if (isset($custom_parts['scheme'])) {
                 $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
                 $this->port = $custom_parts['port'] ?? null;
-                if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) {
-                    $this->base .= ':' . (string)$this->port;
+                if ($this->port && $config->get('system.reverse_proxy_setup') === false) {
+                    $this->base .= ':' . $this->port;
                 }
                 $this->root = $custom_base;
             } else {
@@ -462,8 +462,8 @@ class Uri
     public function port($raw = false)
     {
         $port = $this->port;
-        // If not in raw mode and port is not set, figure it out from scheme.
-        if (!$raw && $port === null) {
+        // If not in raw mode and port is not set or is 0, figure it out from scheme.
+        if (!$raw && !$port) {
             if ($this->scheme === 'http') {
                 $this->port = 80;
             } elseif ($this->scheme === 'https') {
@@ -471,7 +471,7 @@ class Uri
             }
         }
 
-        return $this->port;
+        return $this->port ?: null;
     }
 
     /**
@@ -586,33 +586,38 @@ class Uri
     /**
      * Return relative path to the referrer defaulting to current or given page.
      *
+     * You should set the third parameter to `true` for redirects as long as you came from the same sub-site and language.
+     *
      * @param string|null $default
      * @param string|null $attributes
+     * @param bool $withoutBaseRoute
      * @return string
      */
-    public function referrer($default = null, $attributes = null)
+    public function referrer($default = null, $attributes = null, bool $withoutBaseRoute = false)
     {
         $referrer = $_SERVER['HTTP_REFERER'] ?? null;
 
         // Check that referrer came from our site.
-        $root = $this->rootUrl(true);
-        if ($referrer) {
-            // Referrer should always have host set and it should come from the same base address.
-            if (stripos($referrer, $root) !== 0) {
-                $referrer = null;
-            }
+        if ($withoutBaseRoute) {
+            /** @var Pages $pages */
+            $pages = Grav::instance()['pages'];
+            $base = $pages->baseUrl(null, true);
+        } else {
+            $base = $this->rootUrl(true);
         }
 
-        if (!$referrer) {
+        // Referrer should always have host set and it should come from the same base address.
+        if (!is_string($referrer) || !str_starts_with($referrer, $base)) {
             $referrer = $default ?: $this->route(true, true);
         }
 
+        // Relative path from grav root.
+        $referrer = substr($referrer, strlen($base));
         if ($attributes) {
             $referrer .= $attributes;
         }
 
-        // Return relative path.
-        return substr($referrer, strlen($root));
+        return $referrer;
     }
 
     /**
@@ -648,7 +653,7 @@ class Uri
         return [
             'scheme'    => $this->scheme,
             'host'      => $this->host,
-            'port'      => $this->port,
+            'port'      => $this->port ?: null,
             'user'      => $this->user,
             'pass'      => $this->password,
             'path'      => $path,
@@ -665,7 +670,7 @@ class Uri
      */
     public static function paramsRegex()
     {
-        return '/\/([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/';
+        return '/\/{1,}([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/';
     }
 
     /**
@@ -675,10 +680,15 @@ class Uri
      */
     public static function ip()
     {
+        $ip = 'UNKNOWN';
+
         if (getenv('HTTP_CLIENT_IP')) {
             $ip = getenv('HTTP_CLIENT_IP');
+        } elseif (getenv('HTTP_CF_CONNECTING_IP')) {
+            $ip = getenv('HTTP_CF_CONNECTING_IP');
         } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
-            $ip = getenv('HTTP_X_FORWARDED_FOR');
+            $ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR')));
+            $ip = array_shift($ips);
         } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
             $ip = getenv('HTTP_X_FORWARDED');
         } elseif (getenv('HTTP_FORWARDED_FOR')) {
@@ -687,8 +697,6 @@ class Uri
             $ip = getenv('HTTP_FORWARDED');
         } elseif (getenv('REMOTE_ADDR')) {
             $ip = getenv('REMOTE_ADDR');
-        } else {
-            $ip = 'UNKNOWN';
         }
 
         return $ip;
@@ -1143,11 +1151,8 @@ class Uri
     public static function isValidUrl($url)
     {
         $regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/';
-        if (preg_match($regex, $url)) {
-            return true;
-        }
 
-        return false;
+        return (bool)preg_match($regex, $url);
     }
 
     /**
@@ -1258,7 +1263,7 @@ class Uri
             $this->port = null;
         }
 
-        if ($this->hasStandardPort()) {
+        if ($this->port === 0 || $this->hasStandardPort()) {
             $this->port = null;
         }
 
@@ -1298,7 +1303,7 @@ class Uri
      */
     protected function hasStandardPort()
     {
-        return ($this->port === 80 || $this->port === 443);
+        return (!$this->port || $this->port === 80 || $this->port === 443);
     }
 
     /**
@@ -1311,11 +1316,13 @@ class Uri
         if ($parts === false) {
             throw new RuntimeException('Malformed URL: ' . $url);
         }
+        $port = (int)($parts['port'] ?? 0);
+
         $this->scheme = $parts['scheme'] ?? null;
         $this->user = $parts['user'] ?? null;
         $this->password = $parts['pass'] ?? null;
         $this->host = $parts['host'] ?? null;
-        $this->port = isset($parts['port']) ? (int)$parts['port'] : null;
+        $this->port = $port ?: null;
         $this->path = $parts['path'] ?? '';
         $this->query = $parts['query'] ?? '';
         $this->fragment = $parts['fragment'] ?? null;
@@ -1498,7 +1505,7 @@ class Uri
      * @param string $delimiter
      * @return string
      */
-    private function processParams($uri, $delimiter = ':')
+    private function processParams(string $uri, string $delimiter = ':'): string
     {
         if (strpos($uri, $delimiter) !== false) {
             preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);

+ 24 - 13
system/src/Grav/Common/User/Access.php

@@ -166,9 +166,9 @@ abstract class Utils
 
         if ($locator->isStream($path)) {
             $path = $locator->findResource($path, true);
-        } elseif (!Utils::startsWith($path, GRAV_ROOT)) {
+        } elseif (!static::startsWith($path, GRAV_ROOT)) {
             $base_url = Grav::instance()['base_url'];
-            $path = GRAV_ROOT . '/' . ltrim(Utils::replaceFirstOccurrence($base_url, '', $path), '/');
+            $path = GRAV_ROOT . '/' . ltrim(static::replaceFirstOccurrence($base_url, '', $path), '/');
         }
 
         return $path;
@@ -628,6 +628,23 @@ abstract class Utils
         return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
     }
 
+    /**
+     * Generates a random string with configurable length, prefix and suffix.
+     * Unlike the built-in `uniqid()`, this string is non-conflicting and safe
+     *
+     * @param int $length
+     * @param array $options
+     * @return string
+     * @throws Exception
+     */
+    public static function uniqueId(int $length = 13, array $options = []): string
+    {
+        $options = array_merge(['prefix' => '', 'suffix' => ''], $options);
+        $bytes = random_bytes(ceil($length / 2));
+
+        return $options['prefix'] . substr(bin2hex($bytes), 0, $length) . $options['suffix'];
+    }
+
     /**
      * Provides the ability to download a file to the browser
      *
@@ -750,13 +767,13 @@ abstract class Utils
         if (is_string($http_accept)) {
             $negotiator = new Negotiator();
 
-            $supported_types = Utils::getSupportPageTypes(['html', 'json']);
-            $priorities = Utils::getMimeTypes($supported_types);
+            $supported_types = static::getSupportPageTypes(['html', 'json']);
+            $priorities = static::getMimeTypes($supported_types);
 
             $media_type = $negotiator->getBest($http_accept, $priorities);
             $mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
 
-            return Utils::getExtensionByMime($mimetype);
+            return static::getExtensionByMime($mimetype);
         }
 
         return 'html';
@@ -791,13 +808,7 @@ abstract class Utils
 
         $media_types = Grav::instance()['config']->get('media.types');
 
-        if (isset($media_types[$extension])) {
-            if (isset($media_types[$extension]['mime'])) {
-                return $media_types[$extension]['mime'];
-            }
-        }
-
-        return $default;
+        return $media_types[$extension]['mime'] ?? $default;
     }
 
     /**
@@ -1739,7 +1750,7 @@ abstract class Utils
     {
         $enc_url = preg_replace_callback(
             '%[^:/@?&=#]+%usD',
-            function ($matches) {
+            static function ($matches) {
                 return urlencode($matches[0]);
             },
             $url

+ 4 - 5
system/src/Grav/Common/Yaml.php

@@ -102,11 +102,10 @@ class CleanCommand extends Command
         'vendor/dragonmantank/cron-expression/composer.json',
         'vendor/dragonmantank/cron-expression/tests',
         'vendor/dragonmantank/cron-expression/CHANGELOG.md',
-        'vendor/enshrined/svg-sanitize/tests',
-        'vendor/enshrined/svg-sanitize/.gitignore',
-        'vendor/enshrined/svg-sanitize/.travis.yml',
-        'vendor/enshrined/svg-sanitize/composer.json',
-        'vendor/enshrined/svg-sanitize/phpunit.xml',
+        'vendor/rhukster/dom-sanitizer/tests',
+        'vendor/rhukster/dom-sanitizer/.gitignore',
+        'vendor/rhukster/dom-sanitizer/composer.json',
+        'vendor/rhukster/dom-sanitizer/composer.lock',
         'vendor/erusev/parsedown/composer.json',
         'vendor/erusev/parsedown/phpunit.xml.dist',
         'vendor/erusev/parsedown/.travis.yml',

+ 1 - 1
system/src/Grav/Console/Cli/ClearCacheCommand.php

@@ -24,7 +24,7 @@ use function array_slice;
  * Collection of objects stored into a filesystem.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends AbstractLazyCollection<TKey,T>
  * @mplements FileCollectionInterface<TKey,T>

+ 8 - 1
system/src/Grav/Framework/Collection/AbstractIndexCollection.php

@@ -20,7 +20,7 @@ use function count;
 
 /**
  * Abstract Index Collection.
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @implements CollectionInterface<TKey,T>
  */
@@ -361,6 +361,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * @param int $start
      * @param int|null $limit
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     public function limit($start, $limit = null)
     {
@@ -371,6 +372,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * Reverse the order of the items.
      *
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     public function reverse()
     {
@@ -381,6 +383,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * Shuffle items.
      *
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     public function shuffle()
     {
@@ -397,6 +400,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      * @param array $keys
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     public function select(array $keys)
     {
@@ -415,6 +419,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      * @param array $keys
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     public function unselect(array $keys)
     {
@@ -469,6 +474,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      * @param array $entries Elements.
      * @return static
+     * @phpstan-return static<TKey,T>
      */
     protected function createFrom(array $entries)
     {
@@ -521,6 +527,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
     /**
      * @param array|null $entries
      * @return CollectionInterface
+     * @phpstan-return T
      */
     abstract protected function loadCollection(array $entries = null): CollectionInterface;
 

+ 1 - 1
system/src/Grav/Framework/Collection/AbstractLazyCollection.php

@@ -15,7 +15,7 @@ use Doctrine\Common\Collections\AbstractLazyCollection as BaseAbstractLazyCollec
  * General JSON serializable collection.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends BaseAbstractLazyCollection<TKey,T>
  * @implements CollectionInterface<TKey,T>

+ 1 - 1
system/src/Grav/Framework/Collection/ArrayCollection.php

@@ -15,7 +15,7 @@ use Doctrine\Common\Collections\ArrayCollection as BaseArrayCollection;
  * General JSON serializable collection.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends BaseArrayCollection<TKey,T>
  * @implements CollectionInterface<TKey,T>

+ 1 - 1
system/src/Grav/Framework/Collection/CollectionInterface.php

@@ -16,7 +16,7 @@ use JsonSerializable;
  * Collection Interface.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends Collection<TKey,T>
  */

+ 1 - 1
system/src/Grav/Framework/Collection/FileCollection.php

@@ -13,7 +13,7 @@ namespace Grav\Framework\Collection;
  * Collection of objects stored into a filesystem.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends AbstractFileCollection<TKey,T>
  */

+ 1 - 1
system/src/Grav/Framework/Collection/FileCollectionInterface.php

@@ -15,7 +15,7 @@ use Doctrine\Common\Collections\Selectable;
  * Collection of objects stored into a filesystem.
  *
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @extends CollectionInterface<TKey,T>
  * @extends Selectable<TKey,T>

+ 41 - 5
system/src/Grav/Framework/Compat/Serializable.php

@@ -47,7 +47,7 @@ use function is_callable;
  * @package Grav\Framework\Flex
  * @template T
  */
-class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
+class FlexDirectory implements FlexDirectoryInterface
 {
     use FlexAuthorizeTrait;
 
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
+        $uri = $this->getDirectoryConfigUri($name);
+
+        // If configuration is found in main configuration, use it.
+        if (str_starts_with($uri, 'config://')) {
+            $path = str_replace('/', '.', substr($uri, 9, -5));
+
+            return (array)$grav['config']->get($path);
+        }
+
+        // Load the configuration file.
+        $filename = $locator->findResource($uri, true);
         if ($filename === false) {
             return [];
         }
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
      * @param array $call
      * @return void
      */
-    protected function dynamicFlexField(array &$field, $property, array $call)
+    protected function dynamicFlexField(array &$field, $property, array $call): void
     {
         $params = (array)$call['params'];
         $object = $call['object'] ?? null;
         $method = array_shift($params);
+        $not = false;
+        if (str_starts_with($method, '!')) {
+            $method = substr($method, 1);
+            $not = true;
+        } elseif (str_starts_with($method, 'not ')) {
+            $method = substr($method, 4);
+            $not = true;
+        }
+        $method = trim($method);
 
         if ($object && method_exists($object, $method)) {
             $value = $object->{$method}(...$params);
             if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
-                $field[$property] = array_merge_recursive($field[$property], $value);
+                $value = $this->mergeArrays($field[$property], $value);
+            }
+            $field[$property] = $not ? !$value : $value;
+        }
+    }
+
+    /**
+     * @param array $array1
+     * @param array $array2
+     * @return array
+     */
+    protected function mergeArrays(array $array1, array $array2): array
+    {
+        foreach ($array2 as $key => $value) {
+            if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
+                $array1[$key] = $this->mergeArrays($array1[$key], $value);
             } else {
-                $field[$property] = $value;
+                $array1[$key] = $value;
             }
         }
+
+        return $array1;
     }
 
     /**

+ 18 - 3
system/src/Grav/Framework/Flex/FlexDirectoryForm.php

@@ -129,6 +129,17 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
         return $this;
     }
 
+    /**
+     * @param string $uniqueId
+     * @return void
+     */
+    public function setUniqueId(string $uniqueId): void
+    {
+        if ($uniqueId !== '') {
+            $this->uniqueid = $uniqueId;
+        }
+    }
+
     /**
      * @param string $name
      * @param mixed $default
@@ -318,11 +329,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     }
 
     /**
-     * @param string $field
-     * @param string $filename
+     * @param string|null $field
+     * @param string|null $filename
      * @return Route|null
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
         return null;
     }
@@ -453,7 +464,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     protected function doSerialize(): array
     {
         return $this->doTraitSerialize() + [
+                'form' => $this->form,
                 'directory' => $this->directory,
+                'flexName' => $this->flexName
             ];
     }
 
@@ -465,7 +478,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     {
         $this->doTraitUnserialize($data);
 
+        $this->form = $data['form'];
         $this->directory = $data['directory'];
+        $this->flexName = $data['flexName'];
     }
 
     /**

+ 39 - 7
system/src/Grav/Framework/Flex/FlexForm.php

@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $this->name = $name;
         $this->setObject($object);
-        $this->setName($object->getFlexType(), $name);
+
+        if (isset($options['form']['name'])) {
+            // Use custom form name.
+            $this->flexName = $options['form']['name'];
+        } else {
+            // Use standard form name.
+            $this->setName($object->getFlexType(), $name);
+        }
         $this->setId($this->getName());
 
         $uniqueId = $options['unique_id'] ?? null;
@@ -165,6 +172,17 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
         return $this;
     }
 
+    /**
+     * @param string $uniqueId
+     * @return void
+     */
+    public function setUniqueId(string $uniqueId): void
+    {
+        if ($uniqueId !== '') {
+            $this->uniqueid = $uniqueId;
+        }
+    }
+
     /**
      * @param string $name
      * @param mixed $default
@@ -371,22 +389,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
-            return null;
+            /** @var Route $route */
+            $route = Grav::instance()['route'];
+
+            return $route->withExtension('json')->withGravParam('task', 'media.upload');
         }
 
         return $object->route('/edit.json/task:media.upload');
     }
 
     /**
-     * @param string $field
-     * @param string $filename
+     * @param string|null $field
+     * @param string|null $filename
      * @return Route|null
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
-            return null;
+            /** @var Route $route */
+            $route = Grav::instance()['route'];
+
+            return $route->withExtension('json')->withGravParam('task', 'media.delete');
         }
 
         return $object->route('/edit.json/task:media.delete');
@@ -536,7 +560,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     protected function doSerialize(): array
     {
         return $this->doTraitSerialize() + [
+                'items' => $this->items,
+                'form' => $this->form,
                 'object' => $this->object,
+                'flexName' => $this->flexName,
+                'submitMethod' => $this->submitMethod,
             ];
     }
 
@@ -548,7 +576,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $this->doTraitUnserialize($data);
 
-        $this->object = $data['object'];
+        $this->items = $data['items'] ?? null;
+        $this->form = $data['form'] ?? null;
+        $this->object = $data['object'] ?? null;
+        $this->flexName = $data['flexName'] ?? null;
+        $this->submitMethod = $data['submitMethod'] ?? null;
     }
 
     /**

+ 5 - 1
system/src/Grav/Framework/Flex/FlexFormFlash.php

@@ -540,6 +540,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
      */
     protected function createFrom(array $entries, string $keyField = null)
     {
+        /** @phpstan-var static<T,C> $index */
         $index = new static($entries, $this->getFlexDirectory());
         $index->setKeyField($keyField ?? $this->_keyField);
 
@@ -630,7 +631,10 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
      */
     protected function loadCollection(array $entries = null): CollectionInterface
     {
-        return $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
+        /** @var C $collection */
+        $collection = $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
+
+        return $collection;
     }
 
     /**

+ 48 - 7
system/src/Grav/Framework/Flex/FlexObject.php

@@ -44,6 +44,7 @@ use function is_array;
 use function is_object;
 use function is_scalar;
 use function is_string;
+use function json_encode;
 
 /**
  * Class FlexObject
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
     /** @var array */
     private $_meta;
     /** @var array */
+    protected $_original;
+    /** @var array */
     protected $_changes;
     /** @var string */
     protected $storage_key;
@@ -298,7 +301,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 
         $weight = 0;
         foreach ($properties as $property) {
-            $weight += $this->searchNestedProperty($property, $search, $options);
+            if (strpos($property, '.')) {
+                $weight += $this->searchNestedProperty($property, $search, $options);
+            } else {
+                $weight += $this->searchProperty($property, $search, $options);
+            }
         }
 
         return $weight > 0 ? min($weight, 1) : 0;
@@ -365,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function searchProperty(string $property, string $search, array $options = null): float
     {
-        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
+        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
         $value = $this->getProperty($property);
 
         return $this->searchValue($property, $value, $search, $options);
@@ -379,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function searchNestedProperty(string $property, string $search, array $options = null): float
     {
-        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
+        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
         if ($property === 'key') {
             $value = $this->getKey();
         } else {
@@ -436,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return 0;
     }
 
+    /**
+     * Get original data before update
+     *
+     * @return array
+     */
+    public function getOriginalData(): array
+    {
+        return $this->_original ?? [];
+    }
+
     /**
      * Get any changes based on data sent to update
      *
@@ -649,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
             }
 
             // Store the changes
-            $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
+            $this->_original = $this->getElements();
+            $this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
         }
 
         if ($files && method_exists($this, 'setUpdatedMedia')) {
@@ -687,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return $this->create($key);
     }
 
+    /**
+     * @param UserInterface|null $user
+     */
+    public function check(UserInterface $user = null): void
+    {
+        // If user has been provided, check if the user has permissions to save this object.
+        if ($user && !$this->isAuthorized('save', null, $user)) {
+            throw new \RuntimeException('Forbidden', 403);
+        }
+    }
+
     /**
      * {@inheritdoc}
      * @see FlexObjectInterface::save()
@@ -805,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function getForm(string $name = '', array $options = null)
     {
-        if (!isset($this->_forms[$name])) {
-            $this->_forms[$name] = $this->createFormObject($name, $options);
+        $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
+        if (!isset($this->_forms[$hash])) {
+            $this->_forms[$hash] = $this->createFormObject($name, $options);
         }
 
-        return $this->_forms[$name];
+        return $this->_forms[$hash];
     }
 
     /**
@@ -1059,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return $action;
     }
 
+    /**
+     * Method to reset blueprints if the type changes.
+     *
+     * @return void
+     * @since 1.7.18
+     */
+    protected function resetBlueprints(): void
+    {
+        $this->_blueprint = [];
+    }
+
     // DEPRECATED METHODS
 
     /**

+ 1 - 1
system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php

@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
  * Interface FlexDirectoryInterface
  * @package Grav\Framework\Flex\Interfaces
  */
-interface FlexDirectoryInterface
+interface FlexDirectoryInterface extends FlexAuthorizeInterface
 {
     /**
      * @return bool

+ 2 - 2
system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php

@@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
     /**
      * Get route for deleting files by AJAX.
      *
-     * @param string $field     Field where the file is associated into.
-     * @param string $filename  Filename for the file.
+     * @param string|null $field     Field where the file is associated into.
+     * @param string|null $filename  Filename for the file.
      * @return Route|null       Returns Route object or null if file uploads are not enabled.
      */
     public function getFileDeleteAjaxRoute($field, $filename);

+ 1 - 0
system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php

@@ -51,6 +51,7 @@ interface FlexIndexInterface extends FlexCollectionInterface
      *
      * @param string|null $keyField Switch key field of the collection.
      * @return static  Returns a new Flex Collection with new key field.
+     * @phpstan-return static<T>
      * @api
      */
     public function withKeyField(string $keyField = null);

+ 4 - 4
system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php

@@ -50,7 +50,7 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
     /** @var array|null */
     protected $_reorder;
     /** @var FlexPageObject|null */
-    protected $_original;
+    protected $_originalObject;
 
     /**
      * Clone page.
@@ -264,7 +264,7 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
      */
     public function getOriginal()
     {
-        return $this->_original;
+        return $this->_originalObject;
     }
 
     /**
@@ -276,8 +276,8 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
      */
     public function storeOriginal(): void
     {
-        if (null === $this->_original) {
-            $this->_original = clone $this;
+        if (null === $this->_originalObject) {
+            $this->_originalObject = clone $this;
         }
     }
 

+ 1 - 0
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php

@@ -322,6 +322,7 @@ trait PageLegacyTrait
         $parentKey = $parent ? $parent->getKey() : '';
         if ($this instanceof FlexPageObject) {
             $key = trim($parentKey . '/' . $this->folder(), '/');
+            $key = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $key);
         } else {
             $key = trim($parentKey . '/' . basename($this->getKey()), '/');
         }

Някои файлове не бяха показани, защото твърде много файлове са промени