浏览代码

page evenement

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
 # v1.7.14
 ## 04/29/2021
 ## 04/29/2021
 
 

+ 0 - 1
CODE_OF_CONDUCT.md

@@ -1,7 +1,6 @@
 # ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
 # ![](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)
 [![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)
 [![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)
  [![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",
         "miljar/php-exif": "^0.6",
         "composer/ca-bundle": "^1.2",
         "composer/ca-bundle": "^1.2",
         "dragonmantank/cron-expression": "^1.2",
         "dragonmantank/cron-expression": "^1.2",
-        "phive/twig-extensions-deferred": "^1.0",
         "willdurand/negotiation": "^3.0",
         "willdurand/negotiation": "^3.0",
         "itsgoingd/clockwork": "^5.0",
         "itsgoingd/clockwork": "^5.0",
-        "enshrined/svg-sanitize": "~0.13",
         "symfony/http-client": "^4.4",
         "symfony/http-client": "^4.4",
-        "composer/semver": "^1.4"
+        "composer/semver": "^1.4",
+        "rhukster/dom-sanitizer": "^1.0"
     },
     },
     "require-dev": {
     "require-dev": {
         "codeception/codeception": "^4.1",
         "codeception/codeception": "^4.1",
@@ -93,7 +92,8 @@
     },
     },
     "autoload": {
     "autoload": {
         "psr-4": {
         "psr-4": {
-            "Grav\\": "system/src/Grav"
+            "Grav\\": "system/src/Grav",
+            "Twig\\": "system/src/Twig"
         },
         },
         "files": [
         "files": [
             "system/defines.php"
             "system/defines.php"
@@ -107,8 +107,8 @@
     "scripts": {
     "scripts": {
         "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
         "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
         "post-create-project-cmd": "bin/grav install",
         "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",
         "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": "vendor/bin/codecept run unit",
         "test-windows": "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') {
 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) {
     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>");
         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
               label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
               placeholder: '-lah'
               placeholder: '-lah'
             .at:
             .at:
-              type: cron
+              type: text
+              wrapper_classes: cron-selector
               label: PLUGIN_ADMIN.SCHEDULER_RUNAT
               label: PLUGIN_ADMIN.SCHEDULER_RUNAT
               help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
               help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
               placeholder: '* * * * *'
               placeholder: '* * * * *'

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

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

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

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

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

@@ -28,6 +28,10 @@ types:
     type: image
     type: image
     thumb: media/thumb-webp.png
     thumb: media/thumb-webp.png
     mime: image/webp
     mime: image/webp
+  avif:
+    type: image
+    thumb: media/thumb.png
+    mime: image/avif
   gif:
   gif:
     type: animated
     type: animated
     thumb: media/thumb-gif.png
     thumb: media/thumb-gif.png
@@ -91,7 +95,7 @@ types:
   aif:
   aif:
     type: audio
     type: audio
     thumb: media/thumb-aif.png
     thumb: media/thumb-aif.png
-    mime: audio/aif
+    mime: audio/aiff
   txt:
   txt:
     type: file
     type: file
     thumb: media/thumb-txt.png
     thumb: media/thumb-txt.png
@@ -207,7 +211,7 @@ types:
   js:
   js:
     type: file
     type: file
     thumb: media/thumb-js.png
     thumb: media/thumb-js.png
-    mime: application/javascript
+    mime: text/javascript
   json:
   json:
     type: file
     type: file
     thumb: media/thumb-json.png
     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)
   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_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_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.)
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   gzip: false                                    # GZip compress the page output
   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_timestamp: false                  # Enable asset timestamps
   enable_asset_sri: false                        # Enable asset SRI
   enable_asset_sri: false                        # Enable asset SRI
   collections:
   collections:
-    jquery: system://assets/jquery/jquery-2.x.min.js
+    jquery: system://assets/jquery/jquery-3.x.min.js
 
 
 errors:
 errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
   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
 // Some standard defines
 define('GRAV', true);
 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_SCHEMA', '1.7.0_2020-11-20_1');
 define('GRAV_TESTING', false);
 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
     BAD_DATE: Data invàlida
     AGO: abans
     AGO: abans
     FROM_NOW: des d'ara
     FROM_NOW: des d'ara
+    JUST_NOW: Ara mateix
     SECOND: segon
     SECOND: segon
     MINUTE: minut
     MINUTE: minut
     HOUR: hora
     HOUR: hora
@@ -48,6 +49,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
     VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
     INVALID_INPUT: 'Entrada no vàlida a'
     INVALID_INPUT: 'Entrada no vàlida a'
     MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
     MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
+    XSS_ISSUES: "Detectats potencials problemes XSS al camp '%s'"
   MONTHS_OF_THE_YEAR:
   MONTHS_OF_THE_YEAR:
     - 'Gener'
     - 'Gener'
     - 'Febrer'
     - 'Febrer'
@@ -69,3 +71,17 @@ GRAV:
     - 'Divendres'
     - 'Divendres'
     - 'Dissabte'
     - 'Dissabte'
     - 'Diumenge'
     - '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'
     '/(quiz)zes$/i': '\1'
     '/(alias|status)es$/i': '\1'
     '/(alias|status)es$/i': '\1'
     '/([octop|vir])i$/i': '\1us'
     '/([octop|vir])i$/i': '\1us'
+    '/(n)ews$/i': '\1ouvelles'
   INFLECTOR_UNCOUNTABLE:
   INFLECTOR_UNCOUNTABLE:
     - 'équipement'
     - 'équipement'
     - 'information'
     - 'information'
@@ -58,10 +59,10 @@ GRAV:
     MONTH: mois
     MONTH: mois
     YEAR: année
     YEAR: année
     DECADE: décennie
     DECADE: décennie
-    SEC: s
-    MIN: m
-    HR: h
-    WK: sem
+    SEC: sec.
+    MIN: min.
+    HR: hr.
+    WK: sem.
     MO: m
     MO: m
     YR: an
     YR: an
     DEC: déc
     DEC: déc
@@ -84,6 +85,7 @@ GRAV:
     VALIDATION_FAIL: '<b>La validation a échoué :</b>'
     VALIDATION_FAIL: '<b>La validation a échoué :</b>'
     INVALID_INPUT: 'Saisie non valide'
     INVALID_INPUT: 'Saisie non valide'
     MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
     MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
+    XSS_ISSUES: "Erreurs XSS probablement détectées dans le champ '%s'"
   MONTHS_OF_THE_YEAR:
   MONTHS_OF_THE_YEAR:
     - 'janvier'
     - 'janvier'
     - 'février'
     - 'février'
@@ -105,6 +107,8 @@ GRAV:
     - 'vendredi'
     - 'vendredi'
     - 'samedi'
     - 'samedi'
     - 'dimanche'
     - 'dimanche'
+  YES: "Oui"
+  NO: "Non"
   CRON:
   CRON:
     EVERY: chaque
     EVERY: chaque
     EVERY_HOUR: toutes les heures
     EVERY_HOUR: toutes les heures
@@ -118,7 +122,7 @@ GRAV:
     TEXT_DOW: ' sur <b/>'
     TEXT_DOW: ' sur <b/>'
     TEXT_MONTH: ' de <b />'
     TEXT_MONTH: ' de <b />'
     TEXT_DOM: ' sur <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
     ERROR2: Nombre invalide d'éléments
     ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
     ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
     ERROR4: Expression non reconnue
     ERROR4: Expression non reconnue

+ 3 - 0
system/languages/gl.yaml

@@ -104,6 +104,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Fallou a validación:</b>'
     VALIDATION_FAIL: '<b>Fallou a validación:</b>'
     INVALID_INPUT: 'Entrada incorrecta en'
     INVALID_INPUT: 'Entrada incorrecta en'
     MISSING_REQUIRED_FIELD: 'Falta un campo requirido:'
     MISSING_REQUIRED_FIELD: 'Falta un campo requirido:'
+    XSS_ISSUES: "Detectáronse posibles problemas XSS no campo '% s'"
   MONTHS_OF_THE_YEAR:
   MONTHS_OF_THE_YEAR:
     - 'xaneiro'
     - 'xaneiro'
     - 'febreiro'
     - 'febreiro'
@@ -125,6 +126,8 @@ GRAV:
     - 'venres'
     - 'venres'
     - 'sábado'
     - 'sábado'
     - 'domingo'
     - 'domingo'
+  YES: "Si"
+  NO: "Non"
   CRON:
   CRON:
     EVERY: cada
     EVERY: cada
     EVERY_HOUR: Cada hora
     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```"
   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:
   INFLECTOR_PLURALS:
     '/(quiz)$/i': '\1zes'
     '/(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:
   INFLECTOR_UNCOUNTABLE:
-    - 'peralatan'
-    - 'informasi'
-    - 'nasi'
-    - 'uang'
-    - 'spesies'
-    - 'rangkaian'
-    - 'ikan'
-    - 'domba'
+    - 'Peralatan'
+    - 'Informasi '
+    - 'Nasi'
+    - 'Uang'
+    - 'Jenis'
+    - 'Seri'
+    - 'Ikan'
+    - 'Domba'
   INFLECTOR_IRREGULAR:
   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'
     'move': 'pindahkan'
+  INFLECTOR_ORDINALS:
+    'default': 'ke'
+    'first': 'pertama'
+    'second': 'nd'
+    'third': 'rd'
   NICETIME:
   NICETIME:
-    NO_DATE_PROVIDED: Tanggal tidak tersedia
+    NO_DATE_PROVIDED: Tidak ada tanggal yang disediakan
     BAD_DATE: Format tanggal salah
     BAD_DATE: Format tanggal salah
     AGO: yang lalu
     AGO: yang lalu
-    FROM_NOW: dari saat ini
+    FROM_NOW: dari sekarang
     JUST_NOW: baru saja
     JUST_NOW: baru saja
     SECOND: detik
     SECOND: detik
     MINUTE: menit
     MINUTE: menit
@@ -32,12 +78,12 @@ GRAV:
     MONTH: bulan
     MONTH: bulan
     YEAR: tahun
     YEAR: tahun
     DECADE: dekade
     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
     DEC: desimal
     SECOND_PLURAL: detik
     SECOND_PLURAL: detik
     MINUTE_PLURAL: menit
     MINUTE_PLURAL: menit
@@ -47,17 +93,18 @@ GRAV:
     MONTH_PLURAL: bulan
     MONTH_PLURAL: bulan
     YEAR_PLURAL: tahun
     YEAR_PLURAL: tahun
     DECADE_PLURAL: dekade
     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
     DEC_PLURAL: dekade
   FORM:
   FORM:
     VALIDATION_FAIL: '<b>Validasi gagal:</b>'
     VALIDATION_FAIL: '<b>Validasi gagal:</b>'
     INVALID_INPUT: 'Input tidak valid di'
     INVALID_INPUT: 'Input tidak valid di'
     MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
     MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
+    XSS_ISSUES: "Isu berpotensial XSS terdeteksi dalam baris %s"
   MONTHS_OF_THE_YEAR:
   MONTHS_OF_THE_YEAR:
     - 'Januari'
     - 'Januari'
     - 'Februari'
     - 'Februari'
@@ -76,22 +123,25 @@ GRAV:
     - 'Selasa'
     - 'Selasa'
     - 'Rabu'
     - 'Rabu'
     - 'Kamis'
     - 'Kamis'
-    - 'Jumat'
+    - 'Jum''at'
     - 'Sabtu'
     - 'Sabtu'
     - 'Minggu'
     - 'Minggu'
+  YES: "Ya"
+  NO: "Tidak"
   CRON:
   CRON:
     EVERY: Setiap
     EVERY: Setiap
     EVERY_HOUR: Setiap jam
     EVERY_HOUR: Setiap jam
     EVERY_MINUTE: Setiap menit
     EVERY_MINUTE: Setiap menit
     EVERY_DAY_OF_WEEK: Setiap hari selama seminggu
     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
     EVERY_MONTH: setiap bulan
     TEXT_PERIOD: Setiap <b />
     TEXT_PERIOD: Setiap <b />
+    TEXT_MINS: 'dalam <b />  menit setelah jam yang lalu'
     TEXT_TIME: ' pada <b />:<b />'
     TEXT_TIME: ' pada <b />:<b />'
     TEXT_DOW: ' pada <b />'
     TEXT_DOW: ' pada <b />'
     TEXT_MONTH: ' pada <b />'
     TEXT_MONTH: ' pada <b />'
     TEXT_DOM: ' pada <b />'
     TEXT_DOM: ' pada <b />'
     ERROR1: Tag %s tidak didukung!
     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>'
     VALIDATION_FAIL: '<b>Falha na validação:</b>'
     INVALID_INPUT: 'Dados inseridos são inválidos em'
     INVALID_INPUT: 'Dados inseridos são inválidos em'
     MISSING_REQUIRED_FIELD: 'Campo obrigatório em falta:'
     MISSING_REQUIRED_FIELD: 'Campo obrigatório em falta:'
+    XSS_ISSUES: "Potenciais problemas de XSS detectados no campo '%s'"
   MONTHS_OF_THE_YEAR:
   MONTHS_OF_THE_YEAR:
     - 'Janeiro'
     - 'Janeiro'
     - 'Fevereiro'
     - 'Fevereiro'
@@ -125,6 +126,8 @@ GRAV:
     - 'Sexta-feira'
     - 'Sexta-feira'
     - 'Sábado'
     - 'Sábado'
     - 'Domingo'
     - 'Domingo'
+  YES: "Sim"
+  NO: "Não"
   CRON:
   CRON:
     EVERY: cada
     EVERY: cada
     EVERY_HOUR: cada hora
     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'
     - 'Cuma'
     - 'Cumartesi'
     - 'Cumartesi'
     - 'Pazar'
     - 'Pazar'
+  YES: "Evet"
+  NO: "Hayır"
   CRON:
   CRON:
     EVERY: her
     EVERY: her
     EVERY_HOUR: saatte bir
     EVERY_HOUR: saatte bir

+ 16 - 1
system/languages/uk.yaml

@@ -38,7 +38,9 @@ GRAV:
     YR_PLURAL: 年
     YR_PLURAL: 年
     DEC_PLURAL: 十年
     DEC_PLURAL: 十年
   FORM:
   FORM:
-    MISSING_REQUIRED_FIELD: 遺漏必填欄位:
+    VALIDATION_FAIL: '<b>確驗證失敗:</b>'
+    INVALID_INPUT: '無效輸入:'
+    MISSING_REQUIRED_FIELD: '遺漏必填欄位:'
   MONTHS_OF_THE_YEAR:
   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 */
         /** @var UniformResourceLocator $locator */
         $locator = $grav['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->assets_url = $locator->findResource('asset://', false);
 
 
         $this->config($asset_config);
         $this->config($asset_config);
@@ -164,10 +164,19 @@ class Assets extends PropertyObject
 
 
         // More than one asset
         // More than one asset
         if (is_array($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])) {
         } elseif (isset($this->collections[$asset])) {
             array_shift($args);
             array_shift($args);
@@ -201,8 +210,13 @@ class Assets extends PropertyObject
     protected function addType($collection, $type, $asset, $options)
     protected function addType($collection, $type, $asset, $options)
     {
     {
         if (is_array($asset)) {
         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;
             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\Uri;
 use Grav\Common\Utils;
 use Grav\Common\Utils;
 use Grav\Framework\Object\PropertyObject;
 use Grav\Framework\Object\PropertyObject;
+use RocketTheme\Toolbox\File\File;
 use SplFileInfo;
 use SplFileInfo;
 
 
 /**
 /**
@@ -91,6 +92,10 @@ abstract class BaseAsset extends PropertyObject
      */
      */
     public function init($asset, $options)
     public function init($asset, $options)
     {
     {
+        if (!$asset) {
+            return false;
+        }
+
         $config = Grav::instance()['config'];
         $config = Grav::instance()['config'];
         $uri = Grav::instance()['uri'];
         $uri = Grav::instance()['uri'];
 
 
@@ -182,16 +187,21 @@ abstract class BaseAsset extends PropertyObject
     public static function integrityHash($input)
     public static function integrityHash($input)
     {
     {
         $grav = Grav::instance();
         $grav = Grav::instance();
+        $uri = $grav['uri'];
 
 
         $assetsConfig = $grav['config']->get('system.assets');
         $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 '';
         return '';
@@ -253,6 +263,6 @@ abstract class BaseAsset extends PropertyObject
      */
      */
     protected function cssRewrite($file, $dir, $local)
     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;
 namespace Grav\Common\Assets;
 
 
-use Grav\Common\Assets\BaseAsset;
 use Grav\Common\Assets\Traits\AssetUtilsTrait;
 use Grav\Common\Assets\Traits\AssetUtilsTrait;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Grav;
 use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
 use Grav\Common\Utils;
@@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
         $uri = Grav::instance()['uri'];
         $uri = Grav::instance()['uri'];
 
 
         $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
         $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);
         $this->assets_url = $locator->findResource('asset://', false);
     }
     }
 
 
@@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.css';
         $file = $uid . '.css';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
         $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 {
         } else {
             //if nothing found get out of here!
             //if nothing found get out of here!
             if (empty($assets)) {
             if (empty($assets)) {
@@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
 
 
             // Write file
             // Write file
             if (trim($buffer) !== '') {
             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';
         $file = $uid . '.js';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
         $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 {
         } else {
             //if nothing found get out of here!
             //if nothing found get out of here!
             if (empty($assets)) {
             if (empty($assets)) {
@@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
 
 
             // Write file
             // Write file
             if (trim($buffer) !== '') {
             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, '/');
                 $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]);
             return str_replace($matches[2], $new_url, $matches[0]);
         }, $file);
         }, $file);

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

@@ -68,8 +68,6 @@ trait AssetUtilsTrait
     protected function gatherLinks(array $assets, $css = true)
     protected function gatherLinks(array $assets, $css = true)
     {
     {
         $buffer = '';
         $buffer = '';
-
-
         foreach ($assets as $id => $asset) {
         foreach ($assets as $id => $asset) {
             $local = true;
             $local = true;
 
 
@@ -135,7 +133,7 @@ trait AssetUtilsTrait
 
 
         $imports = [];
         $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];
             $imports[] = $matches[0];
 
 
             return '';
             return '';
@@ -156,6 +154,10 @@ trait AssetUtilsTrait
         $no_key = ['loading'];
         $no_key = ['loading'];
 
 
         foreach ($this->attributes as $key => $value) {
         foreach ($this->attributes as $key => $value) {
+            if ($value === null) {
+                continue;
+            }
+
             if (is_numeric($key)) {
             if (is_numeric($key)) {
                 $key = $value;
                 $key = $value;
             }
             }
@@ -196,7 +198,7 @@ trait AssetUtilsTrait
         }
         }
 
 
         if ($this->timestamp) {
         if ($this->timestamp) {
-            if (Utils::contains($asset, '?') || $querystring) {
+            if ($querystring || Utils::contains($asset, '?')) {
                 $querystring .=  '&' . $this->timestamp;
                 $querystring .=  '&' . $this->timestamp;
             } else {
             } else {
                 $querystring .= '?' . $this->timestamp;
                 $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, '/');
             $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...");
             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);
         $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
 
 
         // Cache key allows us to invalidate all cache on configuration changes.
         // 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->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
         $this->driver_setting = $this->config->get('system.cache.driver');
         $this->driver_setting = $this->config->get('system.cache.driver');
         $this->driver = $this->getCacheDriver();
         $this->driver = $this->getCacheDriver();
@@ -618,11 +618,7 @@ class Cache extends Getters
      */
      */
     public function isVolatileDriver($setting)
     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;
     public static $environment;
 
 
+    /** @var string */
+    public static $securityFile = 'config://security.yaml';
+
     /** @var array */
     /** @var array */
     protected $streams = [
     protected $streams = [
         'user' => [
         'user' => [
@@ -390,12 +393,19 @@ class Setup extends Data
 
 
             if (!$locator->findResource('environment://config', true)) {
             if (!$locator->findResource('environment://config', true)) {
                 // If environment does not have its own directory, remove it from the lookup.
                 // 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);
                 $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_file = CompiledYamlFile::instance($filename);
             $security_content = (array)$security_file->content();
             $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 */
     /** @var string|null */
     protected $scope;
     protected $scope;
 
 
-    /** @var BlueprintSchema */
+    /** @var BlueprintSchema|null */
     protected $blueprintSchema;
     protected $blueprintSchema;
 
 
     /** @var object|null */
     /** @var object|null */
@@ -54,7 +54,7 @@ class Blueprint extends BlueprintForm
      */
      */
     public function __clone()
     public function __clone()
     {
     {
-        if ($this->blueprintSchema) {
+        if (null !== $this->blueprintSchema) {
             $this->blueprintSchema = clone $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] ?? [];
         return $this->types[$name] ?? [];
     }
     }
 
 
+    /**
+     * @param string $name
+     * @return array|null
+     */
+    public function getNestedRules(string $name)
+    {
+        return $this->getNested($name);
+    }
+
     /**
     /**
      * Validate data against blueprints.
      * Validate data against blueprints.
      *
      *
@@ -317,6 +326,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                     $toggle = [];
                     $toggle = [];
                 }
                 }
                 // Recursively fetch the items.
                 // 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);
                 $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
             } else {
             } else {
                 $field = $this->get($value);
                 $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()
     public function blueprints()
     {
     {
-        if (!$this->blueprints) {
+        if (null === $this->blueprints) {
             $this->blueprints = new Blueprint();
             $this->blueprints = new Blueprint();
         } elseif (is_callable($this->blueprints)) {
         } elseif (is_callable($this->blueprints)) {
             // Lazy load blueprints.
             // Lazy load blueprints.

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

@@ -519,17 +519,32 @@ class Validation
             return false;
             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)
     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)
     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);
             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));
             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();
             return $item->toArray();
         }, $data) : $data->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.
      * Shift first directory out of the path.
      *
      *
      * @param string $path
      * @param string $path
-     * @return string
+     * @return string|null
      */
      */
     public static function shift(&$path)
     public static function shift(&$path)
     {
     {
@@ -371,7 +371,7 @@ abstract class Folder
             return;
             return;
         }
         }
 
 
-        if (strpos($target, $source) === 0) {
+        if (strpos($target, $source . '/') === 0) {
             throw new RuntimeException('Cannot move folder to itself');
             throw new RuntimeException('Cannot move folder to itself');
         }
         }
 
 
@@ -417,7 +417,8 @@ abstract class Folder
 
 
         if (!$success) {
         if (!$success) {
             $error = error_get_last();
             $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.
         // 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...');
             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...');
             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...');
             throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
         }
         }
 
 
-        // Get real path for our folder
-        $rootPath = realpath($source);
-
         $files = $this->getArchiveFiles($rootPath);
         $files = $this->getArchiveFiles($rootPath);
 
 
         $status && $status([
         $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\FlexGravTrait;
 use Grav\Common\Flex\Traits\FlexObjectTrait;
 use Grav\Common\Flex\Traits\FlexObjectTrait;
+use Grav\Common\Media\Interfaces\MediaInterface;
 use Grav\Framework\Flex\Traits\FlexMediaTrait;
 use Grav\Framework\Flex\Traits\FlexMediaTrait;
 use function is_array;
 use function is_array;
 
 
@@ -21,7 +22,7 @@ use function is_array;
  *
  *
  * @package Grav\Common\Flex
  * @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 FlexGravTrait;
     use FlexObjectTrait;
     use FlexObjectTrait;
@@ -42,7 +43,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
 
 
         // Handle media fields.
         // Handle media fields.
         $settings = $this->getFieldSettings($name);
         $settings = $this->getFieldSettings($name);
-        if ($settings['media_field'] ?? false === true) {
+        if (($settings['media_field'] ?? false) === true) {
             return $this->parseFileProperty($value, $settings);
             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\PageCollectionInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Utils;
 use Grav\Common\Utils;
-use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 use Grav\Framework\Flex\Pages\FlexPageCollection;
 use Grav\Framework\Flex\Pages\FlexPageCollection;
 use Collator;
 use Collator;
 use InvalidArgumentException;
 use InvalidArgumentException;
@@ -159,7 +158,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      */
      */
     public function addPage(PageInterface $page)
     public function addPage(PageInterface $page)
     {
     {
-        if (!$page instanceof FlexObjectInterface) {
+        if (!$page instanceof PageObject) {
             throw new InvalidArgumentException('$page is not a flex page.');
             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');
         throw new RuntimeException(__METHOD__ . '(): Not Implemented');
     }
     }
 
 
+    /**
+     * Set current page.
+     */
+    public function setCurrent(string $path): void
+    {
+        throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+    }
+
     /**
     /**
      * Return previous item.
      * Return previous item.
      *
      *
@@ -392,8 +399,8 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
             $i = count($manual);
             $i = count($manual);
             $new_list = [];
             $new_list = [];
             foreach ($list as $key => $dummy) {
             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) {
                 if ($order === false) {
                     $order = $i++;
                     $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
      * 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
      * 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
      * @param string|null $field
      * @return static
      * @return static
      * @throws Exception
      * @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 = [];
         $entries = [];
         foreach ($this as $key => $object) {
         foreach ($this as $key => $object) {
@@ -449,7 +456,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
 
 
             $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
             $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
 
 
-            if ($date >= $start && (!$end || $date <= $end)) {
+            if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
                 $entries[$key] = $object;
                 $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);
         $element = parent::get($key);
+        if (null === $element) {
+            return null;
+        }
+
         if (isset($params)) {
         if (isset($params)) {
             $element = $element->getTranslation(ltrim($params, '.'));
             $element = $element->getTranslation(ltrim($params, '.'));
         }
         }
@@ -331,7 +335,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      */
      */
     protected function filterByParent(array $filters)
     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();
                     $child_count = $tmp->count();
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
                     $route = $child->getRoute();
                     $route = $child->getRoute();
+                    $route = $route ? ($route->toString(false) ?: '/') : '';
                     $payload = [
                     $payload = [
-                        'item-key' => basename($child->rawRoute() ?? $child->getKey()),
+                        'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
                         'icon' => $icon,
                         'icon' => $icon,
                         'title' => htmlspecialchars($child->menu()),
                         'title' => htmlspecialchars($child->menu()),
                         'route' => [
                         'route' => [
-                            'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
-                            'raw' => $child->rawRoute(),
+                            'display' => htmlspecialchars($route) ?: null,
+                            'raw' => htmlspecialchars($child->rawRoute()),
                         ],
                         ],
                         'modified' => $this->jsDate($child->modified()),
                         'modified' => $this->jsDate($child->modified()),
                         'child_count' => $child_count ?: null,
                         'child_count' => $child_count ?: null,
@@ -834,12 +842,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
     /**
     /**
      * Remove item from the list.
      * Remove item from the list.
      *
      *
-     * @param PageInterface|string|null $key
-     *
-     * @return $this
+     * @param string $key
+     * @return PageObject|null
      * @throws InvalidArgumentException
      * @throws InvalidArgumentException
      */
      */
-    public function remove($key = null)
+    public function remove($key)
     {
     {
         return $this->getCollection()->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
      * 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
      * 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
      * @param string|null $field
      * @return static
      * @return static
      * @throws Exception
      * @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]);
         $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
     public function getRoute($query = []): ?Route
     {
     {
-        $route = $this->route();
-        if (null === $route) {
+        $path = $this->route();
+        if (null === $path) {
             return null;
             return null;
         }
         }
 
 
-        $route = RouteFactory::createFromString($route);
+        $route = RouteFactory::createFromString($path);
         if ($lang = $route->getLanguage()) {
         if ($lang = $route->getLanguage()) {
             $grav = Grav::instance();
             $grav = Grav::instance();
             if (!$grav['config']->get('system.languages.include_default_lang')) {
             if (!$grav['config']->get('system.languages.include_default_lang')) {
@@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
         $this->getFlexDirectory()->reloadIndex();
         $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
      * @param array|bool $reorder
      * @return FlexObject|FlexObjectInterface
      * @return FlexObject|FlexObjectInterface
@@ -293,7 +311,7 @@ class PageObject extends FlexPageObject
         }
         }
 
 
         // Reset original after save events have all been called.
         // Reset original after save events have all been called.
-        $this->_original = null;
+        $this->_originalObject = null;
 
 
         return $instance;
         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();
         $storageKey = $this->getMasterKey();
         $filesystem = Filesystem::getInstance(false);
         $filesystem = Filesystem::getInstance(false);
         $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
         $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
         $newParentKey = $this->getProperty('parent_key');
         $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;
         $order = !$isMoved ? $this->order() : false;
         if ($order !== false) {
         if ($order !== false) {
             $order = (int)$order;
             $order = (int)$order;
@@ -385,10 +413,12 @@ class PageObject extends FlexPageObject
         // Handle special case where ordering isn't given.
         // Handle special case where ordering isn't given.
         if ($ordering === []) {
         if ($ordering === []) {
             if ($order >= 999999) {
             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;
                 $order = 0;
                 foreach ($siblings as $sibling) {
                 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);
                 $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.
         // Add missing siblings into the end of the list, keeping the previous ordering between them.
         foreach ($siblings as $sibling) {
         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)) {
             if (!in_array($basename, $ordering, true)) {
                 $ordering[] = $basename;
                 $ordering[] = $basename;
             }
             }
@@ -421,7 +452,8 @@ class PageObject extends FlexPageObject
         $ordering = array_flip(array_values($ordering));
         $ordering = array_flip(array_values($ordering));
         $count = count($ordering);
         $count = count($ordering);
         foreach ($siblings as $sibling) {
         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 = $ordering[$basename] ?? null;
             $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
             $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
             $sibling->order($newOrder);
             $sibling->order($newOrder);
@@ -500,6 +532,8 @@ class PageObject extends FlexPageObject
         if ($isNew === true && $name === '') {
         if ($isNew === true && $name === '') {
             // Support onBlueprintCreated event just like in Pages::blueprints($template)
             // Support onBlueprintCreated event just like in Pages::blueprints($template)
             $blueprint->set('initialized', true);
             $blueprint->set('initialized', true);
+            $blueprint->setFilename($template);
+
             Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $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();
         $parent = $this->parent();
         $collection = $parent ? $parent->collection('content', false) : null;
         $collection = $parent ? $parent->collection('content', false) : null;
         if (null !== $path && $collection instanceof PageCollectionInterface) {
         if (null !== $path && $collection instanceof PageCollectionInterface) {
-            return $collection->adjacentSibling($path, $direction);
+            $child = $collection->adjacentSibling($path, $direction);
+            if ($child instanceof PageInterface) {
+                return $child;
+            }
         }
         }
 
 
         return false;
         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();
         ] + parent::getCachedMethods();
     }
     }
 
 
+    /**
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return $this->getProperty('readableName');
+    }
+
     /**
     /**
      * Checks user authorization to the action.
      * 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 {
                 } else {
                     $user = parent::find($query, $field);
                     $user = parent::find($query, $field);
                 }
                 }
-                if ($user) {
+                if ($user instanceof UserObject) {
                     return $user;
                     return $user;
                 }
                 }
             }
             }
@@ -123,7 +123,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
      * @param string $key
      * @param string $key
      * @return string
      * @return string
      */
      */
-    protected function filterUsername(string $key)
+    protected function filterUsername(string $key): string
     {
     {
         $storage = $this->getFlexDirectory()->getStorage();
         $storage = $this->getFlexDirectory()->getStorage();
         if (method_exists($storage, 'normalizeKey')) {
         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
      * @param FlexStorageInterface $storage
      * @return void
      * @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.
         // Username can also be number and stored as such.
         $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
         $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
@@ -187,7 +187,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
      * @param array $updated
      * @param array $updated
      * @param array $removed
      * @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));
         $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;
 namespace Grav\Common\Flex\Types\Users;
 
 
+use Closure;
 use Countable;
 use Countable;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\Blueprint;
@@ -31,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Common\User\Traits\UserTrait;
 use Grav\Common\User\Traits\UserTrait;
 use Grav\Framework\File\Formatter\JsonFormatter;
 use Grav\Framework\File\Formatter\JsonFormatter;
 use Grav\Framework\File\Formatter\YamlFormatter;
 use Grav\Framework\File\Formatter\YamlFormatter;
+use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\Flex;
 use Grav\Framework\Flex\Flex;
 use Grav\Framework\Flex\FlexDirectory;
 use Grav\Framework\Flex\FlexDirectory;
 use Grav\Framework\Flex\Storage\FileStorage;
 use Grav\Framework\Flex\Storage\FileStorage;
@@ -75,6 +77,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
     use UserTrait;
     use UserTrait;
     use UserObjectLegacyTrait;
     use UserObjectLegacyTrait;
 
 
+    /** @var Closure|null */
+    static public $authorizeCallable;
+
     /** @var array|null */
     /** @var array|null */
     protected $_uploads_original;
     protected $_uploads_original;
     /** @var FileInterface|null */
     /** @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.
         // Check user access.
         $access = $this->getAccess();
         $access = $this->getAccess();
         $authorized = $access->authorize($action, $scope);
         $authorized = $access->authorize($action, $scope);
@@ -292,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
         return $value;
         return $value;
     }
     }
 
 
+    /**
+     * @return UserGroupIndex
+     */
+    public function getRoles(): UserGroupIndex
+    {
+        return $this->getGroups();
+    }
+
     /**
     /**
      * Convert object into an array.
      * Convert object into an array.
      *
      *
@@ -689,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
 
 
     /**
     /**
      * @param array $files
      * @param array $files
+     * @return void
      */
      */
     protected function setUpdatedMedia(array $files): void
     protected function setUpdatedMedia(array $files): void
     {
     {
@@ -696,10 +719,16 @@ class UserObject extends FlexObject implements UserInterface, Countable
         $locator = Grav::instance()['locator'];
         $locator = Grav::instance()['locator'];
 
 
         $media = $this->getMedia();
         $media = $this->getMedia();
+        if (!$media instanceof MediaUploadInterface) {
+            return;
+        }
+
+        $filesystem = Filesystem::getInstance(false);
 
 
         $list = [];
         $list = [];
         $list_original = [];
         $list_original = [];
         foreach ($files as $field => $group) {
         foreach ($files as $field => $group) {
+            // Ignore files without a field.
             if ($field === '') {
             if ($field === '') {
                 continue;
                 continue;
             }
             }
@@ -707,7 +736,6 @@ class UserObject extends FlexObject implements UserInterface, Countable
 
 
             // Load settings for the field.
             // Load settings for the field.
             $settings = $this->getMediaFieldSettings($field);
             $settings = $this->getMediaFieldSettings($field);
-
             foreach ($group as $filename => $file) {
             foreach ($group as $filename => $file) {
                 if ($file) {
                 if ($file) {
                     // File upload.
                     // File upload.
@@ -722,8 +750,8 @@ class UserObject extends FlexObject implements UserInterface, Countable
                 }
                 }
 
 
                 if ($file) {
                 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'];
                 $self = $settings['self'];
@@ -746,19 +774,25 @@ class UserObject extends FlexObject implements UserInterface, Countable
                     continue;
                     continue;
                 }
                 }
 
 
+                // Calculate path without the retina scaling factor.
+                $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
+
                 $list[$filename] = [$file, $settings];
                 $list[$filename] = [$file, $settings];
 
 
+                $path = str_replace('.', "\n", $field);
                 if (null !== $data) {
                 if (null !== $data) {
                     $data['name'] = $filename;
                     $data['name'] = $filename;
                     $data['path'] = $filepath;
                     $data['path'] = $filepath;
 
 
-                    $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
+                    $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
                 } else {
                 } else {
-                    $this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
+                    $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
                 }
                 }
             }
             }
         }
         }
 
 
+        $this->clearMediaCache();
+
         $this->_uploads = $list;
         $this->_uploads = $list;
         $this->_uploads_original = $list_original;
         $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 */
     /** @var Remote\Packages|null Remote available Packages */
     private $repository;
     private $repository;
     /** @var Remote\GravCore|null Remove Grav Packages */
     /** @var Remote\GravCore|null Remove Grav Packages */
-    public $grav;
+    private $grav;
+    /** @var bool */
+    private $refresh;
+    /** @var callable|null */
+    private $callback;
 
 
     /** @var array Internal cache */
     /** @var array Internal cache */
     protected $cache;
     protected $cache;
@@ -55,13 +59,45 @@ class GPM extends Iterator
     public function __construct($refresh = false, $callback = null)
     public function __construct($refresh = false, $callback = null)
     {
     {
         parent::__construct();
         parent::__construct();
+
+        Folder::create(CACHE_DIR . '/gpm');
+
         $this->cache = [];
         $this->cache = [];
         $this->installed = new Local\Packages();
         $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 = [];
         $items = [];
 
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
             return $items;
         }
         }
 
 
-        $repository = $this->repository['plugins'];
+        $plugins = $repository['plugins'];
 
 
         // local cache to speed things up
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
         if (isset($this->cache[__METHOD__])) {
@@ -278,18 +315,18 @@ class GPM extends Iterator
         }
         }
 
 
         foreach ($this->installed['plugins'] as $slug => $plugin) {
         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;
                 continue;
             }
             }
 
 
             $local_version = $plugin->version ?? 'Unknown';
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $plugins[$slug]->version;
 
 
             if (version_compare($local_version, $remote_version) < 0) {
             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)
     public function getLatestVersionOfPackage($package_name)
     {
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
             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?
         //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;
         return null;
@@ -356,11 +394,12 @@ class GPM extends Iterator
     {
     {
         $items = [];
         $items = [];
 
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
             return $items;
         }
         }
 
 
-        $repository = $this->repository['themes'];
+        $themes = $repository['themes'];
 
 
         // local cache to speed things up
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
         if (isset($this->cache[__METHOD__])) {
@@ -368,18 +407,18 @@ class GPM extends Iterator
         }
         }
 
 
         foreach ($this->installed['themes'] as $slug => $plugin) {
         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;
                 continue;
             }
             }
 
 
             $local_version = $plugin->version ?? 'Unknown';
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $themes[$slug]->version;
 
 
             if (version_compare($local_version, $remote_version) < 0) {
             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)
     public function getReleaseType($package_name)
     {
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
             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?
         //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;
         return null;
@@ -470,7 +510,7 @@ class GPM extends Iterator
      */
      */
     public function getRepositoryPlugins()
     public function getRepositoryPlugins()
     {
     {
-        return $this->repository['plugins'] ?? null;
+        return $this->getRepository()['plugins'] ?? null;
     }
     }
 
 
     /**
     /**
@@ -493,7 +533,7 @@ class GPM extends Iterator
      */
      */
     public function getRepositoryThemes()
     public function getRepositoryThemes()
     {
     {
-        return $this->repository['themes'] ?? null;
+        return $this->getRepository()['themes'] ?? null;
     }
     }
 
 
     /**
     /**
@@ -504,9 +544,31 @@ class GPM extends Iterator
      */
      */
     public function getRepository()
     public function getRepository()
     {
     {
+        if (null === $this->repository) {
+            try {
+                $this->repository = new Remote\Packages($this->refresh, $this->callback);
+            } catch (Exception $e) {}
+        }
+
         return $this->repository;
         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
      * Searches for a Package in the repository
      *
      *

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

@@ -9,6 +9,7 @@
 
 
 namespace Grav\Common;
 namespace Grav\Common;
 
 
+use Composer\Autoload\ClassLoader;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Setup;
 use Grav\Common\Config\Setup;
 use Grav\Common\Helpers\Exif;
 use Grav\Common\Helpers\Exif;
@@ -134,7 +135,7 @@ class Grav extends Container
      *
      *
      * @return void
      * @return void
      */
      */
-    public static function resetInstance()
+    public static function resetInstance(): void
     {
     {
         if (self::$instance) {
         if (self::$instance) {
             // @phpstan-ignore-next-line
             // @phpstan-ignore-next-line
@@ -152,6 +153,13 @@ class Grav extends Container
     {
     {
         if (null === self::$instance) {
         if (null === self::$instance) {
             self::$instance = static::load($values);
             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) {
         } elseif ($values) {
             $instance = self::$instance;
             $instance = self::$instance;
             foreach ($values as $key => $value) {
             foreach ($values as $key => $value) {
@@ -234,7 +242,7 @@ class Grav extends Container
      *
      *
      * @return void
      * @return void
      */
      */
-    public function process()
+    public function process(): void
     {
     {
         if (isset($this->initialized['process'])) {
         if (isset($this->initialized['process'])) {
             return;
             return;
@@ -466,7 +474,7 @@ class Grav extends Container
      * @param int    $code  Redirection code (30x)
      * @param int    $code  Redirection code (30x)
      * @return void
      * @return void
      */
      */
-    public function redirectLangSafe($route, $code = null)
+    public function redirectLangSafe($route, $code = null): void
     {
     {
         if (!$this['uri']->isExternal($route)) {
         if (!$this['uri']->isExternal($route)) {
             $this->redirect($this['pages']->route($route), $code);
             $this->redirect($this['pages']->route($route), $code);
@@ -481,7 +489,7 @@ class Grav extends Container
      * @param ResponseInterface|null $response
      * @param ResponseInterface|null $response
      * @return void
      * @return void
      */
      */
-    public function header(ResponseInterface $response = null)
+    public function header(ResponseInterface $response = null): void
     {
     {
         if (null === $response) {
         if (null === $response) {
             /** @var PageInterface $page */
             /** @var PageInterface $page */
@@ -506,7 +514,7 @@ class Grav extends Container
      *
      *
      * @return void
      * @return void
      */
      */
-    public function setLocale()
+    public function setLocale(): void
     {
     {
         // Initialize Locale if set and configured.
         // Initialize Locale if set and configured.
         if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
         if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
@@ -567,7 +575,7 @@ class Grav extends Container
      *
      *
      * @return void
      * @return void
      */
      */
-    public function shutdown()
+    public function shutdown(): void
     {
     {
         // Prevent user abort allowing onShutdown event to run without interruptions.
         // Prevent user abort allowing onShutdown event to run without interruptions.
         if (function_exists('ignore_user_abort')) {
         if (function_exists('ignore_user_abort')) {
@@ -686,7 +694,7 @@ class Grav extends Container
      *
      *
      * @return void
      * @return void
      */
      */
-    protected function registerServices()
+    protected function registerServices(): void
     {
     {
         foreach (self::$diMap as $serviceKey => $serviceClass) {
         foreach (self::$diMap as $serviceKey => $serviceClass) {
             if (is_int($serviceKey)) {
             if (is_int($serviceKey)) {
@@ -753,12 +761,10 @@ class Grav extends Container
             // unsupported media type, try to download it...
             // unsupported media type, try to download it...
             if ($uri_extension) {
             if ($uri_extension) {
                 $extension = $uri_extension;
                 $extension = $uri_extension;
+            } elseif (isset($path_parts['extension'])) {
+                $extension = $path_parts['extension'];
             } else {
             } else {
-                if (isset($path_parts['extension'])) {
-                    $extension = $path_parts['extension'];
-                } else {
-                    $extension = null;
-                }
+                $extension = null;
             }
             }
 
 
             if ($extension) {
             if ($extension) {
@@ -773,6 +779,6 @@ class Grav extends Container
             return false;
             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)
     public static function processImageHtml($html, PageInterface $page = null)
     {
     {
         $excerpt = static::getExcerptFromHtml($html, 'img');
         $excerpt = static::getExcerptFromHtml($html, 'img');
+        if (null === $excerpt) {
+            return '';
+        }
 
 
         $original_src = $excerpt['element']['attributes']['src'];
         $original_src = $excerpt['element']['attributes']['src'];
         $excerpt['element']['attributes']['href'] = $original_src;
         $excerpt['element']['attributes']['href'] = $original_src;
@@ -61,6 +64,9 @@ class Excerpts
     public static function processLinkHtml($html, PageInterface $page = null)
     public static function processLinkHtml($html, PageInterface $page = null)
     {
     {
         $excerpt = static::getExcerptFromHtml($html, 'a');
         $excerpt = static::getExcerptFromHtml($html, 'a');
+        if (null === $excerpt) {
+            return '';
+        }
 
 
         $original_href = $excerpt['element']['attributes']['href'];
         $original_href = $excerpt['element']['attributes']['href'];
         $excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
         $excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
@@ -89,7 +95,6 @@ class Excerpts
         $excerpt = null;
         $excerpt = null;
         $inner = [];
         $inner = [];
 
 
-        /** @var DOMElement $element */
         foreach ($elements as $element) {
         foreach ($elements as $element) {
             $attributes = [];
             $attributes = [];
             foreach ($element->attributes as $name => $value) {
             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)
     public function tail($filepath, $lines = 1)
     {
     {
-
         $f = $filepath ? @fopen($filepath, 'rb') : false;
         $f = $filepath ? @fopen($filepath, 'rb') : false;
         if ($f === false) {
         if ($f === false) {
             return false;
             return false;
@@ -62,13 +61,12 @@ class LogViewer
         $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
         $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
 
 
         fseek($f, -1, SEEK_END);
         fseek($f, -1, SEEK_END);
-        if (fread($f, 1) != "\n") {
-            $lines -= 1;
+        if (fread($f, 1) !== "\n") {
+            --$lines;
         }
         }
 
 
         // Start reading
         // Start reading
         $output = '';
         $output = '';
-        $chunk = '';
         // While we would like more
         // While we would like more
         while (ftell($f) > 0 && $lines >= 0) {
         while (ftell($f) > 0 && $lines >= 0) {
             // Figure out how far back we should jump
             // Figure out how far back we should jump
@@ -76,7 +74,11 @@ class LogViewer
             // Do the jump (backwards, relative to where we are)
             // Do the jump (backwards, relative to where we are)
             fseek($f, -$seek, SEEK_CUR);
             fseek($f, -$seek, SEEK_CUR);
             // Read a chunk and prepend it to our output
             // 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
             // Jump back to where we started reading
             fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
             fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
             // Decrease our line counter
             // Decrease our line counter
@@ -123,13 +125,13 @@ class LogViewer
      */
      */
     public function parse($line)
     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);
         preg_match($this->pattern, $line, $data);
         if (!isset($data['date'])) {
         if (!isset($data['date'])) {
-            return array();
+            return [];
         }
         }
 
 
         preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
         preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
@@ -138,7 +140,7 @@ class LogViewer
             $data['trace'] = trim($matches[2]);
             $data['trace'] = trim($matches[2]);
         }
         }
 
 
-        return array(
+        return [
             'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
             'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
             'logger' => $data['logger'],
             'logger' => $data['logger'],
             'level' => $data['level'],
             'level' => $data['level'],
@@ -146,7 +148,7 @@ class LogViewer
             'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
             'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
             'context' => json_decode($data['context'], true),
             'context' => json_decode($data['context'], true),
             'extra' => json_decode($data['extra'], 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)
     public function filter(callable $callback = null)
     {
     {
         foreach ($this->items as $key => $value) {
         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]);
                 unset($this->items[$key]);
             }
             }
         }
         }

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

@@ -187,12 +187,7 @@ class LanguageCodes
      */
      */
     public static function getOrientation($code)
     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)
     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',
         'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
         'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
         'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
         'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
         'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
-        'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format'
+        'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'create', 'fill', 'merge'
     ];
     ];
 
 
     /** @var array */
     /** @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\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Form\FormFlashFile;
 use Grav\Framework\Form\FormFlashFile;
+use Grav\Framework\Mime\MimeTypes;
 use Psr\Http\Message\UploadedFileInterface;
 use Psr\Http\Message\UploadedFileInterface;
 use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RuntimeException;
 use RuntimeException;
 use function dirname;
 use function dirname;
+use function in_array;
 
 
 /**
 /**
  * Implements media upload and delete functionality.
  * Implements media upload and delete functionality.
@@ -106,7 +108,7 @@ trait MediaUploadTrait
      *
      *
      * @param array $metadata
      * @param array $metadata
      * @param array|null $settings
      * @param array|null $settings
-     * @return string|null
+     * @return string
      * @throws RuntimeException
      * @throws RuntimeException
      */
      */
     public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string
     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)
         // 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.
         // 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);
             throw new RuntimeException('The mime type does not match to file extension', 400);
         }
         }
 
 
+        $accepted = false;
+        $errors = [];
         foreach ((array)$settings['accept'] as $type) {
         foreach ((array)$settings['accept'] as $type) {
             // Force acceptance of any file when star notation
             // Force acceptance of any file when star notation
             if ($type === '*') {
             if ($type === '*') {
@@ -418,6 +424,17 @@ trait MediaUploadTrait
         $uploadedFile->moveTo($filepath);
         $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.
      * 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 $filename
      * @param string $path
      * @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);
         parent::__construct($items);
 
 
         $this->params = $params;
         $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;
         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.
      * 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
      * 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
      * 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
      * @param string|null $field
      * @return $this
      * @return $this
      * @throws Exception
      * @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 = [];
         $date_range = [];
         foreach ($this->items as $path => $slug) {
         foreach ($this->items as $path => $slug) {
             $page = $this->pages->get($path);
             $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
      * 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
      * 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
      * @param string|null $field
      * @return PageCollectionInterface
      * @return PageCollectionInterface
      * @throws Exception
      * @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
      * 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.
             // Handle custom streams.
             /** @var UniformResourceLocator $locator */
             /** @var UniformResourceLocator $locator */
             $locator = $grav['locator'];
             $locator = $grav['locator'];
-            if ($locator->isStream($url)) {
+            if ($type === 'link' && $locator->isStream($url)) {
                 $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
                 $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
                 $url_parts['path'] = $grav['base_url_relative'] . '/' . $path;
                 $url_parts['path'] = $grav['base_url_relative'] . '/' . $path;
                 unset($url_parts['stream'], $url_parts['scheme']);
                 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) {
         foreach ($iterator as $file => $info) {
             // Ignore folders and Markdown files.
             // 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;
                 continue;
             }
             }
 
 
             // Find out what type we're dealing with
             // 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)) {
             if (!in_array(strtolower($ext), $media_types, true)) {
                 continue;
                 continue;

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

@@ -337,6 +337,37 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
         return $this;
         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.
      * Forward the call to the image processing method.
      *
      *
@@ -344,6 +375,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
      * @param mixed $args
      * @param mixed $args
      * @return $this|mixed
      * @return $this|mixed
      */
      */
+    
     public function __call($method, $args)
     public function __call($method, $args)
     {
     {
         if (!in_array($method, static::$magic_actions, true)) {
         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;
         $this->attributes = $attributes;
 
 
         $source = $medium->reset()->thumbnail('auto')->display('thumbnail');
         $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');
             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.');
             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) {
         if ($var !== null) {
             // make sure first level are arrays
             // make sure first level are arrays
-            array_walk($var, function (&$value) {
+            array_walk($var, static function (&$value) {
                 $value = (array) $value;
                 $value = (array) $value;
             });
             });
             // make sure all values are strings
             // make sure all values are strings
-            array_walk_recursive($var, function (&$value) {
+            array_walk_recursive($var, static function (&$value) {
                 $value = (string) $value;
                 $value = (string) $value;
             });
             });
             $this->taxonomy = $var;
             $this->taxonomy = $var;

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

@@ -196,6 +196,58 @@ class Pages
         return $this->baseRoute($lang) . $route;
         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.
      * Get base URL for Grav pages.
@@ -274,7 +326,7 @@ class Pages
      *
      *
      * @return void
      * @return void
      */
      */
-    public function reset()
+    public function reset(): void
     {
     {
         $this->initialized = false;
         $this->initialized = false;
 
 
@@ -540,9 +592,9 @@ class Pages
         }
         }
 
 
         if (isset($params['dateRange'])) {
         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);
             $collection = $collection->dateRange($start, $end, $field);
         }
         }
 
 
@@ -554,7 +606,7 @@ class Pages
 
 
             if (is_array($sort_flags)) {
             if (is_array($sort_flags)) {
                 $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
                 $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;
                     return $a | $b;
                 }, 0); //merge constant values using bit or
                 }, 0); //merge constant values using bit or
             }
             }
@@ -663,29 +715,39 @@ class Pages
 
 
         switch ($type) {
         switch ($type) {
             case 'all':
             case 'all':
-                return $page->children();
+                $collection = $page->children();
+                break;
             case 'modules':
             case 'modules':
             case 'modular':
             case 'modular':
-                return $page->children()->modules();
+                $collection = $page->children()->modules();
+                break;
             case 'pages':
             case 'pages':
             case 'children':
             case 'children':
-                return $page->children()->pages();
+                $collection = $page->children()->pages();
+                break;
             case 'page':
             case 'page':
             case 'self':
             case 'self':
-                return !$page->root() ? (new Collection())->addPage($page) : new Collection();
+                $collection = !$page->root() ? (new Collection())->addPage($page) : new Collection();
+                break;
             case 'parent':
             case 'parent':
                 $parent = $page->parent();
                 $parent = $page->parent();
                 $collection = new Collection();
                 $collection = new Collection();
-                return $parent ? $collection->addPage($parent) : $collection;
+                $collection = $parent ? $collection->addPage($parent) : $collection;
+                break;
             case 'siblings':
             case 'siblings':
                 $parent = $page->parent();
                 $parent = $page->parent();
-                return $parent ? $parent->children()->remove($page->path()) : new Collection();
+                $collection = $parent ? $parent->children()->remove($page->path()) : new Collection();
+                break;
             case 'descendants':
             case 'descendants':
-                return $this->all($page)->remove($page->path())->pages();
+                $collection = $this->all($page)->remove($page->path())->pages();
+                break;
             default:
             default:
                 // Unknown type; return empty collection.
                 // 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.
         // Build regular expression for all the allowed page extensions.
         $page_extensions = $language->getFallbackPageExtensions();
         $page_extensions = $language->getFallbackPageExtensions();
         $regex = '/^[^\.]*(' . implode('|', array_map(
         $regex = '/^[^\.]*(' . implode('|', array_map(
-            function ($str) {
+            static function ($str) {
                 return preg_quote($str, '/');
                 return preg_quote($str, '/');
             },
             },
             $page_extensions
             $page_extensions

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

@@ -10,6 +10,7 @@
 namespace Grav\Common;
 namespace Grav\Common;
 
 
 use ArrayAccess;
 use ArrayAccess;
+use Composer\Autoload\ClassLoader;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\Data;
 use Grav\Common\Data\Data;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
@@ -42,6 +43,8 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
     protected $active = true;
     protected $active = true;
     /** @var Blueprint|null */
     /** @var Blueprint|null */
     protected $blueprint;
     protected $blueprint;
+    /** @var ClassLoader|null */
+    protected $loader;
 
 
     /**
     /**
      * By default assign all methods as listeners using the default priority.
      * 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
      * @param Config $config
      * @return $this
      * @return $this

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

@@ -143,7 +143,7 @@ class Plugins extends Iterator
                 $instance->setConfig($config);
                 $instance->setConfig($config);
                 // Register autoloader.
                 // Register autoloader.
                 if (method_exists($instance, 'autoload')) {
                 if (method_exists($instance, 'autoload')) {
-                    $instance->autoload();
+                    $instance->setAutoloader($instance->autoload());
                 }
                 }
                 // Register event listeners.
                 // Register event listeners.
                 $events->addSubscriber($instance);
                 $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!
         // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM!
         $grav = Grav::instance();
         $grav = Grav::instance();
+        /** @var UniformResourceLocator $locator */
         $locator = $grav['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)) {
         if (is_file($file)) {
             // Local variables available in the file: $grav, $name, $file
             // Local variables available in the file: $grav, $name, $file
             $class = include_once $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(
             $grav['log']->addWarning(
                 sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
                 sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
             );
             );
-            return null;
         }
         }
 
 
         return $class;
         return $class;

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

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

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

@@ -10,6 +10,8 @@
 namespace Grav\Common\Processors;
 namespace Grav\Common\Processors;
 
 
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Form\Forms;
 use RocketTheme\Toolbox\Event\Event;
 use RocketTheme\Toolbox\Event\Event;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\ServerRequestInterface;
@@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
         $page = $this->container['page'];
         $page = $this->container['page'];
 
 
         if (!$page->routable()) {
         if (!$page->routable()) {
+            $exception = new RequestException($request, 'Page Not Found', 404);
+            $route = $this->container['route'];
             // If no page found, fire event
             // 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->page = null;
             $event = $this->container->fireEvent('onPageNotFound', $event);
             $event = $this->container->fireEvent('onPageNotFound', $event);
 
 
@@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase
 
 
             $task = $this->container['task'];
             $task = $this->container['task'];
             $action = $this->container['action'];
             $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) {
             if ($task) {
-                $event = new Event(['task' => $task, 'page' => $page]);
+                $event = new Event(['task' => $task] + $options);
                 $this->container->fireEvent('onPageTask', $event);
                 $this->container->fireEvent('onPageTask', $event);
                 $this->container->fireEvent('onPageTask.' . $task, $event);
                 $this->container->fireEvent('onPageTask.' . $task, $event);
             } elseif ($action) {
             } elseif ($action) {
-                $event = new Event(['action' => $action, 'page' => $page]);
+                $event = new Event(['action' => $action] + $options);
                 $this->container->fireEvent('onPageAction', $event);
                 $this->container->fireEvent('onPageAction', $event);
                 $this->container->fireEvent('onPageAction.' . $action, $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\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use RocketTheme\Toolbox\Event\Event;
 
 
 /**
 /**
  * Class RenderProcessor
  * Class RenderProcessor
@@ -42,23 +43,27 @@ class RenderProcessor extends ProcessorBase
             return $output;
             return $output;
         }
         }
 
 
-        ob_start();
+        /** @var PageInterface $page */
+        $page = $this->container['page'];
 
 
         // Use internal Grav output.
         // Use internal Grav output.
         $container->output = $output;
         $container->output = $output;
-        $container->fireEvent('onOutputGenerated');
+
+        ob_start();
+
+        $event = new Event(['page' => $page, 'output' => &$container->output]);
+        $container->fireEvent('onOutputGenerated', $event);
 
 
         echo $container->output;
         echo $container->output;
 
 
+        $html = ob_get_clean();
+
         // remove any output
         // remove any output
         $container->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();
         $this->stopTimer();
 
 
         return new Response($page->httpResponseCode(), $page->httpHeaders(), $html);
         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) {
         if ($whenOverlapping) {
             $this->whenOverlapping = $whenOverlapping;
             $this->whenOverlapping = $whenOverlapping;
         } else {
         } else {
-            $this->whenOverlapping = function () {
+            $this->whenOverlapping = static function () {
                 return false;
                 return false;
             };
             };
         }
         }
@@ -390,7 +390,9 @@ class Job
         if (count($this->outputTo) > 0) {
         if (count($this->outputTo) > 0) {
             foreach ($this->outputTo as $file) {
             foreach ($this->outputTo as $file) {
                 $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
                 $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;
 namespace Grav\Common;
 
 
-use enshrined\svgSanitize\Sanitizer;
 use Exception;
 use Exception;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Page\Pages;
 use Grav\Common\Page\Pages;
+use Rhukster\DomSanitizer\DOMSanitizer;
 use function chr;
 use function chr;
 use function count;
 use function count;
 use function is_array;
 use function is_array;
@@ -33,7 +34,7 @@ class Security
     public static function sanitizeSvgString(string $svg): string
     public static function sanitizeSvgString(string $svg): string
     {
     {
         if (Grav::instance()['config']->get('security.sanitize_svg')) {
         if (Grav::instance()['config']->get('security.sanitize_svg')) {
-            $sanitizer = new Sanitizer();
+            $sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
             $sanitized = $sanitizer->sanitize($svg);
             $sanitized = $sanitizer->sanitize($svg);
             if (is_string($sanitized)) {
             if (is_string($sanitized)) {
                 $svg = $sanitized;
                 $svg = $sanitized;
@@ -52,13 +53,20 @@ class Security
     public static function sanitizeSVG(string $file): void
     public static function sanitizeSVG(string $file): void
     {
     {
         if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) {
         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);
             $original_svg = file_get_contents($file);
             $clean_svg = $sanitizer->sanitize($original_svg);
             $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);
                 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');
                 $content = $page->value('content');
 
 
                 $data = ['header' => $header, 'content' => $content];
                 $data = ['header' => $header, 'content' => $content];
-                $results = Security::detectXssFromArray($data);
+                $results = static::detectXssFromArray($data);
 
 
                 if (!empty($results)) {
                 if (!empty($results)) {
                     if ($route) {
                     if ($route) {
@@ -191,7 +199,7 @@ class Security
         $string = urldecode($string);
         $string = urldecode($string);
 
 
         // Convert Hexadecimals
         // 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]));
             return chr(hexdec($m[2]));
         }, $string);
         }, $string);
 
 
@@ -231,7 +239,7 @@ class Security
             }
             }
         }
         }
 
 
-        return false;
+        return null;
     }
     }
 
 
     public static function getXssDefaults(): array
     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\ConfigFileFinder;
 use Grav\Common\Config\Setup;
 use Grav\Common\Config\Setup;
 use Grav\Common\Language\Language;
 use Grav\Common\Language\Language;
+use Grav\Framework\Mime\MimeTypes;
 use Pimple\Container;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
 use Pimple\ServiceProviderInterface;
 use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\File\YamlFile;
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
             return $config;
             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) {
         $container['languages'] = function ($c) {
             return static::languages($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 Grav\Common\Grav;
 use Pimple\Container;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
 use Pimple\ServiceProviderInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 
 /**
 /**
  * Class TaskServiceProvider
  * Class TaskServiceProvider
@@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
     public function register(Container $container)
     public function register(Container $container)
     {
     {
         $container['task'] = function (Grav $c) {
         $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) {
             if (null !== $task) {
                 $task = filter_var($task, FILTER_SANITIZE_STRING);
                 $task = filter_var($task, FILTER_SANITIZE_STRING);
             }
             }
@@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
         };
         };
 
 
         $container['action'] = function (Grav $c) {
         $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) {
             if (null !== $action) {
                 $action = filter_var($action, FILTER_SANITIZE_STRING);
                 $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\Common\Form\FormFlash;
 use Grav\Events\SessionStartEvent;
 use Grav\Events\SessionStartEvent;
 use Grav\Plugin\Form\Forms;
 use Grav\Plugin\Form\Forms;
+use JsonException;
 use function is_string;
 use function is_string;
 
 
 /**
 /**
@@ -128,12 +129,12 @@ class Session extends \Grav\Framework\Session\Session
                 /** @var Uri $uri */
                 /** @var Uri $uri */
                 $uri = $grav['uri'];
                 $uri = $grav['uri'];
                 /** @var Forms|null $form */
                 /** @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);
                 $sessionField = base64_encode($uri->url);
 
 
                 /** @var FormFlash|null $flash */
                 /** @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;
                 $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 mixed $object
      * @param int $time
      * @param int $time
      * @return $this
      * @return $this
+     * @throws JsonException
      */
      */
     public function setFlashCookieObject($name, $object, $time = 60)
     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;
         return $this;
     }
     }
@@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
      *
      *
      * @param string $name
      * @param string $name
      * @return mixed|null
      * @return mixed|null
+     * @throws JsonException
      */
      */
     public function getFlashCookieObject($name)
     public function getFlashCookieObject($name)
     {
     {
         if (isset($_COOKIE[$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;
         return null;

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

@@ -105,7 +105,7 @@ class Taxonomy
             }
             }
         } elseif (is_string($value)) {
         } elseif (is_string($value)) {
             if (!empty($key)) {
             if (!empty($key)) {
-                $taxonomy = $taxonomy . $key;
+                $taxonomy .= $key;
             }
             }
             $this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()];
             $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;
         $grav = $this->grav;
         $config = $this->config;
         $config = $this->config;
         $name = $this->current();
         $name = $this->current();
+        $class = null;
 
 
         /** @var UniformResourceLocator $locator */
         /** @var UniformResourceLocator $locator */
         $locator = $grav['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) {
         if ($file) {
             // Local variables available in the file: $grav, $config, $name, $file
             // Local variables available in the file: $grav, $config, $name, $file
             $class = include $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')) {
         } elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
             $response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
             $response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
@@ -253,12 +243,28 @@ class Themes extends Iterator
             $grav->close($response);
             $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);
             $class = new Theme($grav, $config, $name);
         }
         }
 
 
+        $this->config->set('theme', $config->get('themes.' . $name));
+
         return $class;
         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 Grav\Common\Utils;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use Twig\Extension\AbstractExtension;
 use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
 use Twig\TwigFunction;
 use Twig\TwigFunction;
 
 
 /**
 /**
@@ -30,11 +31,35 @@ class FilesystemExtension extends AbstractExtension
     }
     }
 
 
     /**
     /**
-     * @return TwigFunction[]
+     * @return TwigFilter[]
      */
      */
     public function getFilters()
     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('md5_file', [$this, 'md5_file']),
             new TwigFunction('sha1_file', [$this, 'sha1_file']),
             new TwigFunction('sha1_file', [$this, 'sha1_file']),
             new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']),
             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);
         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
      * @param string $filename
      * @return bool
      * @return bool

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

@@ -90,7 +90,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
      *
      *
      * @return array
      * @return array
      */
      */
-    public function getGlobals()
+    public function getGlobals(): array
     {
     {
         return [
         return [
             'grav' => $this->grav,
             'grav' => $this->grav,
@@ -102,7 +102,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
      *
      *
      * @return array
      * @return array
      */
      */
-    public function getFilters()
+    public function getFilters(): array
     {
     {
         return [
         return [
             new TwigFilter('*ize', [$this, 'inflectorFilter']),
             new TwigFilter('*ize', [$this, 'inflectorFilter']),
@@ -155,10 +155,15 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             new TwigFilter('bool', [$this, 'boolFilter']),
             new TwigFilter('bool', [$this, 'boolFilter']),
             new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
             new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
             new TwigFilter('array', [$this, 'arrayFilter']),
             new TwigFilter('array', [$this, 'arrayFilter']),
+            new TwigFilter('yaml', [$this, 'yamlFilter']),
 
 
             // Object Types
             // Object Types
             new TwigFilter('get_type', [$this, 'getTypeFunc']),
             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
      * @return array
      */
      */
-    public function getFunctions()
+    public function getFunctions(): array
     {
     {
         return [
         return [
             new TwigFunction('array', [$this, 'arrayFilter']),
             new TwigFunction('array', [$this, 'arrayFilter']),
@@ -212,6 +217,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             new TwigFunction('cron', [$this, 'cronFunc']),
             new TwigFunction('cron', [$this, 'cronFunc']),
             new TwigFunction('svg_image', [$this, 'svgImageFunction']),
             new TwigFunction('svg_image', [$this, 'svgImageFunction']),
             new TwigFunction('xss', [$this, 'xssFunc']),
             new TwigFunction('xss', [$this, 'xssFunc']),
+            new TwigFunction('unique_id', [$this, 'uniqueId']),
 
 
             // Translations
             // Translations
             new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
             new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
@@ -220,14 +226,25 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
 
 
             // Object Types
             // Object Types
             new TwigFunction('get_type', [$this, 'getTypeFunc']),
             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
      * @return array
      */
      */
-    public function getTokenParsers()
+    public function getTokenParsers(): array
     {
     {
         return [
         return [
             new TwigTokenParserRender(),
             new TwigTokenParserRender(),
@@ -638,6 +655,20 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
         return implode(', ', $results_parts);
         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
      * @param string $string
      * @return 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
      * @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 admin and tu filter provided, use it
         if (isset($this->grav['admin'])) {
         if (isset($this->grav['admin'])) {
             $numargs = count($args);
             $numargs = count($args);
@@ -1484,7 +1522,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             }
             }
 
 
             //Look for existing class
             //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])) {
                 if (isset($matches[2])) {
                     $new_classes = $matches[2] . $classes;
                     $new_classes = $matches[2] . $classes;
                     $matched = true;
                     $matched = true;
@@ -1500,7 +1538,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
                 $svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
                 $svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
             }
             }
 
 
-            return $svg;
+            return trim($svg);
         }
         }
 
 
         return null;
         return null;

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

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

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

@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
 
 
         $compiler
         $compiler
             ->indent()
             ->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')) {
         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
         $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\Language\LanguageCodes;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Pages;
 use Grav\Common\Page\Pages;
+use Grav\Common\Twig\Exception\TwigException;
 use Grav\Common\Twig\Extension\FilesystemExtension;
 use Grav\Common\Twig\Extension\FilesystemExtension;
 use Grav\Common\Twig\Extension\GravExtension;
 use Grav\Common\Twig\Extension\GravExtension;
 use Grav\Common\Utils;
 use Grav\Common\Utils;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\Event\Event;
 use RocketTheme\Toolbox\Event\Event;
-use Phive\Twig\Extensions\Deferred\DeferredExtension;
 use RuntimeException;
 use RuntimeException;
 use Twig\Cache\FilesystemCache;
 use Twig\Cache\FilesystemCache;
+use Twig\DeferredExtension\DeferredExtension;
 use Twig\Environment;
 use Twig\Environment;
 use Twig\Error\LoaderError;
 use Twig\Error\LoaderError;
+use Twig\Error\RuntimeError;
 use Twig\Extension\CoreExtension;
 use Twig\Extension\CoreExtension;
 use Twig\Extension\DebugExtension;
 use Twig\Extension\DebugExtension;
 use Twig\Extension\StringLoaderExtension;
 use Twig\Extension\StringLoaderExtension;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\ChainLoader;
 use Twig\Loader\ChainLoader;
-use Twig\Loader\ExistsLoaderInterface;
 use Twig\Loader\FilesystemLoader;
 use Twig\Loader\FilesystemLoader;
 use Twig\Profiler\Profile;
 use Twig\Profiler\Profile;
 use Twig\TwigFilter;
 use Twig\TwigFilter;
@@ -404,38 +405,63 @@ class Twig
      */
      */
     public function processSite($format = null, array $vars = [])
     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);
             $output = $this->twig->render($template, $vars + $twig_vars);
         } catch (LoaderError $e) {
         } 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;
         return $output;
@@ -488,25 +514,21 @@ class Twig
         $twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT;
         $twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT;
         $template_file = $this->template($page->template() . $twig_extension);
         $template_file = $this->template($page->template() . $twig_extension);
 
 
-        $page_template = null;
-
         $loader = $this->twig->getLoader();
         $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;
         return $page_template;

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

@@ -160,8 +160,8 @@ class Uri
         $language = $grav['language'];
         $language = $grav['language'];
 
 
         // add the port to the base for non-standard ports
         // 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
         // Handle custom base
@@ -176,8 +176,8 @@ class Uri
             if (isset($custom_parts['scheme'])) {
             if (isset($custom_parts['scheme'])) {
                 $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
                 $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
                 $this->port = $custom_parts['port'] ?? null;
                 $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;
                 $this->root = $custom_base;
             } else {
             } else {
@@ -462,8 +462,8 @@ class Uri
     public function port($raw = false)
     public function port($raw = false)
     {
     {
         $port = $this->port;
         $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') {
             if ($this->scheme === 'http') {
                 $this->port = 80;
                 $this->port = 80;
             } elseif ($this->scheme === 'https') {
             } 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.
      * 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 $default
      * @param string|null $attributes
      * @param string|null $attributes
+     * @param bool $withoutBaseRoute
      * @return string
      * @return string
      */
      */
-    public function referrer($default = null, $attributes = null)
+    public function referrer($default = null, $attributes = null, bool $withoutBaseRoute = false)
     {
     {
         $referrer = $_SERVER['HTTP_REFERER'] ?? null;
         $referrer = $_SERVER['HTTP_REFERER'] ?? null;
 
 
         // Check that referrer came from our site.
         // 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);
             $referrer = $default ?: $this->route(true, true);
         }
         }
 
 
+        // Relative path from grav root.
+        $referrer = substr($referrer, strlen($base));
         if ($attributes) {
         if ($attributes) {
             $referrer .= $attributes;
             $referrer .= $attributes;
         }
         }
 
 
-        // Return relative path.
-        return substr($referrer, strlen($root));
+        return $referrer;
     }
     }
 
 
     /**
     /**
@@ -648,7 +653,7 @@ class Uri
         return [
         return [
             'scheme'    => $this->scheme,
             'scheme'    => $this->scheme,
             'host'      => $this->host,
             'host'      => $this->host,
-            'port'      => $this->port,
+            'port'      => $this->port ?: null,
             'user'      => $this->user,
             'user'      => $this->user,
             'pass'      => $this->password,
             'pass'      => $this->password,
             'path'      => $path,
             'path'      => $path,
@@ -665,7 +670,7 @@ class Uri
      */
      */
     public static function paramsRegex()
     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()
     public static function ip()
     {
     {
+        $ip = 'UNKNOWN';
+
         if (getenv('HTTP_CLIENT_IP')) {
         if (getenv('HTTP_CLIENT_IP')) {
             $ip = 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')) {
         } 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')) {
         } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
             $ip = getenv('HTTP_X_FORWARDED');
             $ip = getenv('HTTP_X_FORWARDED');
         } elseif (getenv('HTTP_FORWARDED_FOR')) {
         } elseif (getenv('HTTP_FORWARDED_FOR')) {
@@ -687,8 +697,6 @@ class Uri
             $ip = getenv('HTTP_FORWARDED');
             $ip = getenv('HTTP_FORWARDED');
         } elseif (getenv('REMOTE_ADDR')) {
         } elseif (getenv('REMOTE_ADDR')) {
             $ip = getenv('REMOTE_ADDR');
             $ip = getenv('REMOTE_ADDR');
-        } else {
-            $ip = 'UNKNOWN';
         }
         }
 
 
         return $ip;
         return $ip;
@@ -1143,11 +1151,8 @@ class Uri
     public static function isValidUrl($url)
     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})*))?/';
         $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;
             $this->port = null;
         }
         }
 
 
-        if ($this->hasStandardPort()) {
+        if ($this->port === 0 || $this->hasStandardPort()) {
             $this->port = null;
             $this->port = null;
         }
         }
 
 
@@ -1298,7 +1303,7 @@ class Uri
      */
      */
     protected function hasStandardPort()
     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) {
         if ($parts === false) {
             throw new RuntimeException('Malformed URL: ' . $url);
             throw new RuntimeException('Malformed URL: ' . $url);
         }
         }
+        $port = (int)($parts['port'] ?? 0);
+
         $this->scheme = $parts['scheme'] ?? null;
         $this->scheme = $parts['scheme'] ?? null;
         $this->user = $parts['user'] ?? null;
         $this->user = $parts['user'] ?? null;
         $this->password = $parts['pass'] ?? null;
         $this->password = $parts['pass'] ?? null;
         $this->host = $parts['host'] ?? null;
         $this->host = $parts['host'] ?? null;
-        $this->port = isset($parts['port']) ? (int)$parts['port'] : null;
+        $this->port = $port ?: null;
         $this->path = $parts['path'] ?? '';
         $this->path = $parts['path'] ?? '';
         $this->query = $parts['query'] ?? '';
         $this->query = $parts['query'] ?? '';
         $this->fragment = $parts['fragment'] ?? null;
         $this->fragment = $parts['fragment'] ?? null;
@@ -1498,7 +1505,7 @@ class Uri
      * @param string $delimiter
      * @param string $delimiter
      * @return string
      * @return string
      */
      */
-    private function processParams($uri, $delimiter = ':')
+    private function processParams(string $uri, string $delimiter = ':'): string
     {
     {
         if (strpos($uri, $delimiter) !== false) {
         if (strpos($uri, $delimiter) !== false) {
             preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);
             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)) {
         if ($locator->isStream($path)) {
             $path = $locator->findResource($path, true);
             $path = $locator->findResource($path, true);
-        } elseif (!Utils::startsWith($path, GRAV_ROOT)) {
+        } elseif (!static::startsWith($path, GRAV_ROOT)) {
             $base_url = Grav::instance()['base_url'];
             $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;
         return $path;
@@ -628,6 +628,23 @@ abstract class Utils
         return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
         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
      * Provides the ability to download a file to the browser
      *
      *
@@ -750,13 +767,13 @@ abstract class Utils
         if (is_string($http_accept)) {
         if (is_string($http_accept)) {
             $negotiator = new Negotiator();
             $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);
             $media_type = $negotiator->getBest($http_accept, $priorities);
             $mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
             $mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
 
 
-            return Utils::getExtensionByMime($mimetype);
+            return static::getExtensionByMime($mimetype);
         }
         }
 
 
         return 'html';
         return 'html';
@@ -791,13 +808,7 @@ abstract class Utils
 
 
         $media_types = Grav::instance()['config']->get('media.types');
         $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(
         $enc_url = preg_replace_callback(
             '%[^:/@?&=#]+%usD',
             '%[^:/@?&=#]+%usD',
-            function ($matches) {
+            static function ($matches) {
                 return urlencode($matches[0]);
                 return urlencode($matches[0]);
             },
             },
             $url
             $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/composer.json',
         'vendor/dragonmantank/cron-expression/tests',
         'vendor/dragonmantank/cron-expression/tests',
         'vendor/dragonmantank/cron-expression/CHANGELOG.md',
         '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/composer.json',
         'vendor/erusev/parsedown/phpunit.xml.dist',
         'vendor/erusev/parsedown/phpunit.xml.dist',
         'vendor/erusev/parsedown/.travis.yml',
         '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.
  * Collection of objects stored into a filesystem.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends AbstractLazyCollection<TKey,T>
  * @extends AbstractLazyCollection<TKey,T>
  * @mplements FileCollectionInterface<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.
  * Abstract Index Collection.
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @implements CollectionInterface<TKey,T>
  * @implements CollectionInterface<TKey,T>
  */
  */
@@ -361,6 +361,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * @param int $start
      * @param int $start
      * @param int|null $limit
      * @param int|null $limit
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     public function limit($start, $limit = null)
     public function limit($start, $limit = null)
     {
     {
@@ -371,6 +372,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * Reverse the order of the items.
      * Reverse the order of the items.
      *
      *
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     public function reverse()
     public function reverse()
     {
     {
@@ -381,6 +383,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      * Shuffle items.
      * Shuffle items.
      *
      *
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     public function shuffle()
     public function shuffle()
     {
     {
@@ -397,6 +400,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      *
      * @param array $keys
      * @param array $keys
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     public function select(array $keys)
     public function select(array $keys)
     {
     {
@@ -415,6 +419,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      *
      * @param array $keys
      * @param array $keys
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     public function unselect(array $keys)
     public function unselect(array $keys)
     {
     {
@@ -469,6 +474,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
      *
      *
      * @param array $entries Elements.
      * @param array $entries Elements.
      * @return static
      * @return static
+     * @phpstan-return static<TKey,T>
      */
      */
     protected function createFrom(array $entries)
     protected function createFrom(array $entries)
     {
     {
@@ -521,6 +527,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
     /**
     /**
      * @param array|null $entries
      * @param array|null $entries
      * @return CollectionInterface
      * @return CollectionInterface
+     * @phpstan-return T
      */
      */
     abstract protected function loadCollection(array $entries = null): CollectionInterface;
     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.
  * General JSON serializable collection.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends BaseAbstractLazyCollection<TKey,T>
  * @extends BaseAbstractLazyCollection<TKey,T>
  * @implements CollectionInterface<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.
  * General JSON serializable collection.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends BaseArrayCollection<TKey,T>
  * @extends BaseArrayCollection<TKey,T>
  * @implements CollectionInterface<TKey,T>
  * @implements CollectionInterface<TKey,T>

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

@@ -16,7 +16,7 @@ use JsonSerializable;
  * Collection Interface.
  * Collection Interface.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends Collection<TKey,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.
  * Collection of objects stored into a filesystem.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends AbstractFileCollection<TKey,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.
  * Collection of objects stored into a filesystem.
  *
  *
  * @package Grav\Framework\Collection
  * @package Grav\Framework\Collection
- * @template TKey
+ * @template TKey of array-key
  * @template T
  * @template T
  * @extends CollectionInterface<TKey,T>
  * @extends CollectionInterface<TKey,T>
  * @extends Selectable<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
  * @package Grav\Framework\Flex
  * @template T
  * @template T
  */
  */
-class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
+class FlexDirectory implements FlexDirectoryInterface
 {
 {
     use FlexAuthorizeTrait;
     use FlexAuthorizeTrait;
 
 
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 
 
         /** @var UniformResourceLocator $locator */
         /** @var UniformResourceLocator $locator */
         $locator = $grav['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) {
         if ($filename === false) {
             return [];
             return [];
         }
         }
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
      * @param array $call
      * @param array $call
      * @return void
      * @return void
      */
      */
-    protected function dynamicFlexField(array &$field, $property, array $call)
+    protected function dynamicFlexField(array &$field, $property, array $call): void
     {
     {
         $params = (array)$call['params'];
         $params = (array)$call['params'];
         $object = $call['object'] ?? null;
         $object = $call['object'] ?? null;
         $method = array_shift($params);
         $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)) {
         if ($object && method_exists($object, $method)) {
             $value = $object->{$method}(...$params);
             $value = $object->{$method}(...$params);
             if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
             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 {
             } 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;
         return $this;
     }
     }
 
 
+    /**
+     * @param string $uniqueId
+     * @return void
+     */
+    public function setUniqueId(string $uniqueId): void
+    {
+        if ($uniqueId !== '') {
+            $this->uniqueid = $uniqueId;
+        }
+    }
+
     /**
     /**
      * @param string $name
      * @param string $name
      * @param mixed $default
      * @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
      * @return Route|null
      */
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
     {
         return null;
         return null;
     }
     }
@@ -453,7 +464,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     protected function doSerialize(): array
     protected function doSerialize(): array
     {
     {
         return $this->doTraitSerialize() + [
         return $this->doTraitSerialize() + [
+                'form' => $this->form,
                 'directory' => $this->directory,
                 'directory' => $this->directory,
+                'flexName' => $this->flexName
             ];
             ];
     }
     }
 
 
@@ -465,7 +478,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     {
     {
         $this->doTraitUnserialize($data);
         $this->doTraitUnserialize($data);
 
 
+        $this->form = $data['form'];
         $this->directory = $data['directory'];
         $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->name = $name;
         $this->setObject($object);
         $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());
         $this->setId($this->getName());
 
 
         $uniqueId = $options['unique_id'] ?? null;
         $uniqueId = $options['unique_id'] ?? null;
@@ -165,6 +172,17 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * @param string $uniqueId
+     * @return void
+     */
+    public function setUniqueId(string $uniqueId): void
+    {
+        if ($uniqueId !== '') {
+            $this->uniqueid = $uniqueId;
+        }
+    }
+
     /**
     /**
      * @param string $name
      * @param string $name
      * @param mixed $default
      * @param mixed $default
@@ -371,22 +389,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
     {
         $object = $this->getObject();
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
         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');
         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
      * @return Route|null
      */
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
     {
         $object = $this->getObject();
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
         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');
         return $object->route('/edit.json/task:media.delete');
@@ -536,7 +560,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     protected function doSerialize(): array
     protected function doSerialize(): array
     {
     {
         return $this->doTraitSerialize() + [
         return $this->doTraitSerialize() + [
+                'items' => $this->items,
+                'form' => $this->form,
                 'object' => $this->object,
                 'object' => $this->object,
+                'flexName' => $this->flexName,
+                'submitMethod' => $this->submitMethod,
             ];
             ];
     }
     }
 
 
@@ -548,7 +576,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
     {
         $this->doTraitUnserialize($data);
         $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)
     protected function createFrom(array $entries, string $keyField = null)
     {
     {
+        /** @phpstan-var static<T,C> $index */
         $index = new static($entries, $this->getFlexDirectory());
         $index = new static($entries, $this->getFlexDirectory());
         $index->setKeyField($keyField ?? $this->_keyField);
         $index->setKeyField($keyField ?? $this->_keyField);
 
 
@@ -630,7 +631,10 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
      */
      */
     protected function loadCollection(array $entries = null): CollectionInterface
     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_object;
 use function is_scalar;
 use function is_scalar;
 use function is_string;
 use function is_string;
+use function json_encode;
 
 
 /**
 /**
  * Class FlexObject
  * Class FlexObject
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
     /** @var array */
     /** @var array */
     private $_meta;
     private $_meta;
     /** @var array */
     /** @var array */
+    protected $_original;
+    /** @var array */
     protected $_changes;
     protected $_changes;
     /** @var string */
     /** @var string */
     protected $storage_key;
     protected $storage_key;
@@ -298,7 +301,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 
 
         $weight = 0;
         $weight = 0;
         foreach ($properties as $property) {
         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;
         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
     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);
         $value = $this->getProperty($property);
 
 
         return $this->searchValue($property, $value, $search, $options);
         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
     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') {
         if ($property === 'key') {
             $value = $this->getKey();
             $value = $this->getKey();
         } else {
         } else {
@@ -436,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return 0;
         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
      * Get any changes based on data sent to update
      *
      *
@@ -649,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
             }
             }
 
 
             // Store the changes
             // 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')) {
         if ($files && method_exists($this, 'setUpdatedMedia')) {
@@ -687,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return $this->create($key);
         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}
      * {@inheritdoc}
      * @see FlexObjectInterface::save()
      * @see FlexObjectInterface::save()
@@ -805,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
      */
     public function getForm(string $name = '', array $options = null)
     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;
         return $action;
     }
     }
 
 
+    /**
+     * Method to reset blueprints if the type changes.
+     *
+     * @return void
+     * @since 1.7.18
+     */
+    protected function resetBlueprints(): void
+    {
+        $this->_blueprint = [];
+    }
+
     // DEPRECATED METHODS
     // DEPRECATED METHODS
 
 
     /**
     /**

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

@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
  * Interface FlexDirectoryInterface
  * Interface FlexDirectoryInterface
  * @package Grav\Framework\Flex\Interfaces
  * @package Grav\Framework\Flex\Interfaces
  */
  */
-interface FlexDirectoryInterface
+interface FlexDirectoryInterface extends FlexAuthorizeInterface
 {
 {
     /**
     /**
      * @return bool
      * @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.
      * 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.
      * @return Route|null       Returns Route object or null if file uploads are not enabled.
      */
      */
     public function getFileDeleteAjaxRoute($field, $filename);
     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.
      * @param string|null $keyField Switch key field of the collection.
      * @return static  Returns a new Flex Collection with new key field.
      * @return static  Returns a new Flex Collection with new key field.
+     * @phpstan-return static<T>
      * @api
      * @api
      */
      */
     public function withKeyField(string $keyField = null);
     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 */
     /** @var array|null */
     protected $_reorder;
     protected $_reorder;
     /** @var FlexPageObject|null */
     /** @var FlexPageObject|null */
-    protected $_original;
+    protected $_originalObject;
 
 
     /**
     /**
      * Clone page.
      * Clone page.
@@ -264,7 +264,7 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
      */
      */
     public function getOriginal()
     public function getOriginal()
     {
     {
-        return $this->_original;
+        return $this->_originalObject;
     }
     }
 
 
     /**
     /**
@@ -276,8 +276,8 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI
      */
      */
     public function storeOriginal(): void
     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() : '';
         $parentKey = $parent ? $parent->getKey() : '';
         if ($this instanceof FlexPageObject) {
         if ($this instanceof FlexPageObject) {
             $key = trim($parentKey . '/' . $this->folder(), '/');
             $key = trim($parentKey . '/' . $this->folder(), '/');
+            $key = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $key);
         } else {
         } else {
             $key = trim($parentKey . '/' . basename($this->getKey()), '/');
             $key = trim($parentKey . '/' . basename($this->getKey()), '/');
         }
         }

部分文件因为文件数量过多而无法显示