43 Ревизии b6730fa8d2 ... adc7d775dc

Автор SHA1 Съобщение Дата
  bach adc7d775dc maj преди 2 години
  bach 131ffd7020 disabled lead the way as it blocks other scrit execution преди 2 години
  ouidade faaab9c4d3 maj преди 2 години
  ouidade c7262233b2 fix bug form devis преди 3 години
  ouidade 2f006c43b0 devis преди 3 години
  ouidade 705918a589 annule changement .gitignore преди 3 години
  ouidade 4239905866 .gitignore преди 3 години
  ouidade e8f529d64a email преди 3 години
  ouidade ef94f03cde maj преди 3 години
  ouidade 12814f64d4 fix path demande de devis преди 3 години
  ouidade 761ebf2eb3 fix design form demande devis преди 3 години
  ouidade 9dce5f34fb Merge branch 'master' of https://figureslibres.io/gogs/kevin/lecampus.net преди 3 години
  ouidade 0baf738c5e script tracking преди 3 години
  ouidade 0f8f4c6ccf maj преди 3 години
  ouidade c25f71c98b fix bug преди 3 години
  ouidade f5cb936c97 maj преди 3 години
  ouidade cfd6acbbb8 Merge branch 'master' into prod преди 3 години
  ouidade c0162ffcaa date début event преди 4 години
  ouidade 235bc5a4b9 Revert "date début event" преди 3 години
  ouidade 66f14b6c0e màj преди 4 години
  ouidade 4ca5c9f82d màj преди 4 години
  ouidade 8bd1b83c5f date début event преди 4 години
  ouidade d3f363e6af portrait equipe css преди 4 години
  ouidade 1464924265 css potrait преди 4 години
  ouidade 9bf8ed00b0 maj leaflet map преди 4 години
  ouidade d2dc72037a maj map преди 4 години
  ouidade 0287c3cc57 lien fichiers scss преди 4 години
  ouidade af967cdbb7 le_campus.yaml преди 4 години
  ouidade 2ff5cec751 module преди 4 години
  ouidade 01afb95428 module valeurs/equipe преди 4 години
  ouidade 416d183223 module valeurs/equipe преди 4 години
  ouidade 0a7a10c92d css modules преди 4 години
  ouidade b8e8055517 modules nos valeurs/equipe преди 4 години
  ouidade 8442dce45c nos valeurs преди 4 години
  ouidade 586a9de49a display nos valeurs преди 4 години
  ouidade 9889afc077 display nos valeurs преди 4 години
  ouidade 3172378468 display nos valeurs преди 4 години
  ouidade c8f59ec4b1 bandeau devis home + packs преди 4 години
  ouidade 1deaf3f44c link linkedin преди 4 години
  ouidade 8860b10368 js преди 4 години
  ouidade 613d932017 function open_pack преди 4 години
  ouidade 047db27266 lien vers themme.css преди 4 години
  ouidade 81285b7135 recup commit dev преди 4 години
променени са 100 файла, в които са добавени 4799 реда и са изтрити 828 реда
  1. 462 0
      CHANGELOG.md
  2. 12 6
      README.md
  3. 1 1
      assets/.gitkeep
  4. BIN
      bin/composer.phar
  5. 3 11
      bin/gpm
  6. 3 11
      bin/grav
  7. 3 11
      bin/plugin
  8. 28 18
      composer.json
  9. 264 344
      composer.lock
  10. 5 5
      fixperms.sh
  11. 12 26
      index.php
  12. 0 1
      system/assets/debugger/phpdebugbar.css
  13. 2 1
      system/blueprints/config/scheduler.yaml
  14. 202 54
      system/blueprints/config/system.yaml
  15. 2 2
      system/blueprints/flex/pages.yaml
  16. 13 0
      system/blueprints/flex/user-accounts.yaml
  17. 1 0
      system/blueprints/flex/user-groups.yaml
  18. 12 0
      system/blueprints/pages/default.yaml
  19. 9 9
      system/blueprints/pages/external.yaml
  20. 3 7
      system/blueprints/pages/partials/security.yaml
  21. 19 2
      system/blueprints/user/account.yaml
  22. 7 3
      system/config/media.yaml
  23. 1986 0
      system/config/mime.yaml
  24. 25 5
      system/config/system.yaml
  25. 5 2
      system/defines.php
  26. BIN
      system/images/watermark.png
  27. 1 1
      system/install.php
  28. 11 0
      system/languages/ar.yaml
  29. 16 0
      system/languages/ca.yaml
  30. 6 6
      system/languages/es.yaml
  31. 9 5
      system/languages/fr.yaml
  32. 3 0
      system/languages/gl.yaml
  33. 81 31
      system/languages/id.yaml
  34. 147 0
      system/languages/mn.yaml
  35. 3 0
      system/languages/pt.yaml
  36. 9 0
      system/languages/si.yaml
  37. 2 0
      system/languages/tr.yaml
  38. 16 1
      system/languages/zh-tw.yaml
  39. 20 3
      system/router.php
  40. 165 0
      system/src/DOMLettersIterator.php
  41. 158 0
      system/src/DOMWordsIterator.php
  42. 179 24
      system/src/Grav/Common/Assets.php
  43. 24 5
      system/src/Grav/Common/Assets/BaseAsset.php
  44. 207 0
      system/src/Grav/Common/Assets/BlockAssets.php
  45. 2 2
      system/src/Grav/Common/Assets/Css.php
  46. 2 2
      system/src/Grav/Common/Assets/InlineCss.php
  47. 2 2
      system/src/Grav/Common/Assets/InlineJs.php
  48. 46 0
      system/src/Grav/Common/Assets/InlineJsModule.php
  49. 2 2
      system/src/Grav/Common/Assets/Js.php
  50. 49 0
      system/src/Grav/Common/Assets/JsModule.php
  51. 43 0
      system/src/Grav/Common/Assets/Link.php
  52. 77 19
      system/src/Grav/Common/Assets/Pipeline.php
  53. 18 11
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  54. 1 1
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  55. 10 1
      system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
  56. 5 6
      system/src/Grav/Common/Backup/Backups.php
  57. 1 1
      system/src/Grav/Common/Browser.php
  58. 4 8
      system/src/Grav/Common/Cache.php
  59. 1 1
      system/src/Grav/Common/Composer.php
  60. 1 1
      system/src/Grav/Common/Config/CompiledBase.php
  61. 1 1
      system/src/Grav/Common/Config/CompiledBlueprints.php
  62. 1 1
      system/src/Grav/Common/Config/CompiledConfig.php
  63. 1 1
      system/src/Grav/Common/Config/CompiledLanguages.php
  64. 1 1
      system/src/Grav/Common/Config/Config.php
  65. 1 1
      system/src/Grav/Common/Config/ConfigFileFinder.php
  66. 1 1
      system/src/Grav/Common/Config/Languages.php
  67. 18 7
      system/src/Grav/Common/Config/Setup.php
  68. 13 12
      system/src/Grav/Common/Data/Blueprint.php
  69. 26 10
      system/src/Grav/Common/Data/BlueprintSchema.php
  70. 1 1
      system/src/Grav/Common/Data/Blueprints.php
  71. 3 2
      system/src/Grav/Common/Data/Data.php
  72. 1 1
      system/src/Grav/Common/Data/DataInterface.php
  73. 60 15
      system/src/Grav/Common/Data/Validation.php
  74. 20 5
      system/src/Grav/Common/Data/ValidationException.php
  75. 6 2
      system/src/Grav/Common/Debugger.php
  76. 1 1
      system/src/Grav/Common/Errors/BareHandler.php
  77. 1 1
      system/src/Grav/Common/Errors/Errors.php
  78. 2 2
      system/src/Grav/Common/Errors/SimplePageHandler.php
  79. 22 1
      system/src/Grav/Common/Errors/SystemFacade.php
  80. 83 10
      system/src/Grav/Common/File/CompiledFile.php
  81. 1 1
      system/src/Grav/Common/File/CompiledJsonFile.php
  82. 1 1
      system/src/Grav/Common/File/CompiledMarkdownFile.php
  83. 1 1
      system/src/Grav/Common/File/CompiledYamlFile.php
  84. 1 1
      system/src/Grav/Common/Filesystem/Archiver.php
  85. 66 58
      system/src/Grav/Common/Filesystem/Folder.php
  86. 1 1
      system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
  87. 1 1
      system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
  88. 4 5
      system/src/Grav/Common/Filesystem/ZipArchiver.php
  89. 1 1
      system/src/Grav/Common/Flex/FlexCollection.php
  90. 1 1
      system/src/Grav/Common/Flex/FlexIndex.php
  91. 4 3
      system/src/Grav/Common/Flex/FlexObject.php
  92. 1 1
      system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
  93. 1 1
      system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
  94. 1 1
      system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
  95. 1 1
      system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
  96. 1 1
      system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php
  97. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php
  98. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
  99. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
  100. 46 18
      system/src/Grav/Common/Flex/Types/Pages/PageCollection.php

+ 462 - 0
CHANGELOG.md

@@ -1,3 +1,465 @@
+# v1.7.42.3
+## 07/18/2023
+
+2. [](#improved)
+   * Fixed a typo in `Utils::isDangerousFunction`
+
+# v1.7.42.2
+## 07/18/2023
+
+2. [](#improved)
+   * In `Utils::isDangerousFunction`, handle double `\\` in `|map` twig filter to mitigate SSTI attack
+   * Better handle empty email in `Validatoin::typeEmail()`
+
+# v1.7.42.1
+## 06/15/2023
+
+2. [](#improved)
+   * Quick fix for `isDangerousFunction` when `$name` was a closure [#3727](https://github.com/getgrav/grav/issues/3727)
+
+# v1.7.42
+## 06/14/2023
+
+1. [](#new)
+   * Added a new `system.languages.debug` option that adds a `<span class="translate-debug"></span>` around strings translated with `|t`. This can be styled by the theme as needed.
+1. [](#improved)
+   * More robust SSTI handling in `filter`, `map`, and `reduce` Twig filters and functions
+   * Various SSTI improvements `Utils::isDangerousFunction()`
+1. [](#bugfix)
+   * Fixed Twig `|map()` allowing code execution
+   * Fixed Twig `|reduce()` allowing code execution
+
+# v1.7.41.2
+## 06/01/2023
+
+1. [](#improved)
+   * Added the ability to set a configurable 'key' for the Twig Cache Tag: `{% cache 'my-key' 600 %}`
+1. [](#bugfix)
+   * Fixed an issue with special characters in slug's would cause redirect loops
+
+# v1.7.41.1
+## 05/10/2023
+
+1. [](#bugfix)
+   * Fixed certain UTF-8 characters breaking `Truncator` class [#3716](https://github.com/getgrav/grav/issues/3716)
+
+# v1.7.41
+## 05/09/2023
+
+1. [](#improved)
+   * Removed `FILTER_SANITIZE_STRING` input filter in favor of `htmlspecialchars(strip_tags())` for PHP 8.2+
+   * Added `GRAV_SANITIZE_STRING` constant to replace `FILTER_SANITIZE_STRING` for PHP 8.2+
+   * Support non-deprecated style dynamic properties in `Parsedown` class via `ParseDownGravTrait` for PHP 8.2+
+   * Modified `Truncator` to not use deprecated `mb_convert_encoding()` for PHP 8.2+
+   * Fixed passing null into `mb_strpos()` deprecated for PHP 8.2+
+   * Updated internal `TwigDeferredExtension` to be PHP 8.2+ compatible
+   * Upgraded `getgrav/image` fork to take advantage of various PHP 8.2+ fixes
+   * Use `UserGroupObject::groupNames` method in blueprints for PHP 8.2+
+   * Comment out `files-upload` deprecated message as this is not going to be removed
+   * Added various public `Twig` class variables used by admin to address deprecated messages for PHP 8.2+
+   * Added `parse_url` to list of PHP functions supported in Twig Extension
+   * Added support for dynamic functions in `Parsedown` to stop deprecation messages in PHP 8.2+
+ 
+# v1.7.40
+## 03/22/2023
+
+1. [](#new)
+    * Added a new `timestamp: true|false` option for individual assets
+1. [](#improved)
+    * Removed outdated `xcache` setting [#3615](https://github.com/getgrav/grav/pull/3615)
+    * Updated `robots.txt` [#3625](https://github.com/getgrav/grav/pull/3625)
+1. [](#bugfix)
+    * Fixed `force_ssl` redirect in case of undefined hostname [#3702](https://github.com/getgrav/grav/pull/3702)
+    * Fixed an issue with duplicate identical page paths
+    * Fixed `BlueprintSchema:flattenData` to properly handle ignored fields
+    * Fixed LogViewer regex greediness [#3684](https://github.com/getgrav/grav/pull/3684)
+    * Fixed `whoami` command [#3695](https://github.com/getgrav/grav/pull/3695)
+
+# v1.7.39.4
+## 02/22/2023
+
+1. [](#bugfix)
+    * Reverted a reorganization of `account.yaml` that caused username to be disabled [admin#2344](https://github.com/getgrav/grav-plugin-admin/issues/2344)
+
+# v1.7.39.3
+## 02/21/2023
+
+1. [](#bugfix)
+    * Fix for overzealous modular page template rendering fix in 1.7.39 causing Feed plugin to break [#3689](https://github.com/getgrav/grav/issues/3689)
+
+# v1.7.39.2
+## 02/20/2023
+
+1. [](#bugfix)
+    * Fix for invalid session breaking Flex Accounts (when switching from Regular to Flex)
+
+# v1.7.39.1
+## 02/20/2023
+
+1. [](#bugfix)
+    * Fix for broken image CSS with the latest version of DebugBar
+
+# v1.7.39
+## 02/19/2023
+
+1. [](#improved)
+    * Vendor library updates to latest versions
+1. [](#bugfix)
+    * Various PHP 8.2 fixes
+    * Fixed an issue with modular pages rendering thew wrong template when dynamically changing the page
+    * Fixed an issue with `email` validation that was failing on UTF-8 characters. Following best practices and now only check for `@` and length.
+    * Fixed PHPUnit tests to remove deprecation warnings
+
+# v1.7.38
+## 01/02/2023
+
+1. [](#new)
+    * New `onBeforeSessionStart()` event to be used to store data lost during session regeneration (e.g. login)
+1. [](#improved)
+   * Vendor library updates to latest versions
+   * Updated `bin/composer.phar` to latest `2.4.4` version [#3627](https://github.com/getgrav/grav/issues/3627)
+1. [](#bugfix)
+   * Don't fail hard if pages recurse with same path
+   * Github workflows security hardening [#3624](https://github.com/getgrav/grav/pull/3624)
+
+# v1.7.37.1
+## 10/05/2022
+
+1. [](#bugfix)
+    * Fixed a bad return type [#3630](https://github.com/getgrav/grav/issues/3630)
+
+# v1.7.37
+## 10/05/2022
+
+1. [](#new)
+    * Added new `onPageHeaders()` event to allow for header modification as needed
+    * Added a `system.pages.dirs` configuration option to allow for configurable paths, and multiple page paths
+    * Added new `Pages::getSimplePagesHash` which is useful for caching pages specific data
+    * Updated to latest vendor libraries
+1. [](#bugfix)
+    * An attempt to workaround windows reading locked file issue [getgrav/grav-plugin-admin#2299](https://github.com/getgrav/grav-plugin-admin/issues/2299)
+    * Force user index file to be updated to fix email addresses [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229)
+
+# v1.7.36
+## 09/08/2022
+
+1. [](#new)
+    * Added `authorize-*@:` support for Flex blueprints, e.g. `authorize-disabled@: not delete` disables the field if user does not have access to delete object
+    * Added support for `flex-ignore@` to hide all the nested fields in the blueprint
+1. [](#bugfix)
+    * Fixed login with a capitalised email address when using old users [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229)
+
+# v1.7.35
+## 08/04/2022
+
+1. [](#new)
+   * Added support for `multipart/form-data` content type in PUT and PATCH requests
+   * Added support for object relationships
+   * Added variables `$environment` (string), `$request` (PSR-7 ServerRequestInterface|null) and `$uri` (PSR-7 Uri|null) to be used in `setup.php`
+1. [](#improved)
+   * Minor vendor updates
+
+# v1.7.34
+## 06/14/2022
+
+1. [](#new)
+    * Added back Yiddish to Language Codes [#3336](https://github.com/getgrav/grav/pull/3336)
+    * Ignore upcoming `media.json` file in media
+1. [](#bugfix)
+    * Regression: Fixed saving page with a new language causing cache corruption [getgrav/grav-plugin-admin#2282](https://github.com/getgrav/grav-plugin-admin/issues/2282)
+    * Fixed a potential fatal error when using watermark in images
+    * Fixed `bin/grav install` command with arbitrary destination folder name
+    * Fixed Twig `|filter()` allowing code execution
+    * Fixed login and user search by email not being case-insensitive when using Flex Users
+
+# v1.7.33
+## 04/25/2022
+
+1. [](#improved)
+    * When saving yaml and markdown, create also a cached version of the file and recompile it in opcache
+2. [](#bugfix)
+    * Fixed missing changes in **yaml** & **markdown** files if saved multiple times during the same second because of a caching issue
+    * Fixed XSS check not detecting onX events without quotes
+    * Fixed default collection ordering in pages admin
+
+# v1.7.32
+## 03/28/2022
+
+1. [](#new)
+    * Added `|replace_last(search, replace)` filter
+    * Added `parseurl` Twig function to expose PHP's `parse_url` function
+2. [](#improved)
+    * Added multi-language support for page routes in `Utils::url()`
+    * Set default maximum length for text fields
+      - `password`: 256
+      - `email`: 320
+      - `text`, `url`, `hidden`, `commalist`: 2048
+      - `text` (multiline), `textarea`: 65536
+3. [](#bugfix)
+   * Fixed issue with `system.cache.gzip: true` resulted in "Fetch Failed" for PHP 8.0.17 and PHP 8.1.4 [PHP issue #8218](https://github.com/php/php-src/issues/8218)
+   * Fix for multi-lang issues with Security Report
+   * Fixed page search not working with selected language [#3316](https://github.com/getgrav/grav/issues/3316)
+
+# v1.7.31
+## 03/14/2022
+
+1. [](#new)
+   * Added new local Multiavatar (local generation). **This will be default in Grav 1.8**
+   * Added support to get image size for SVG vector images [#3533](https://github.com/getgrav/grav/pull/3533)
+   * Added XSS check for uploaded SVG files before they get stored
+   * Fixed phpstan issues (All level 2, Framework level 5)
+2. [](#improved)
+   * Moved Accounts out of Experimental section of System configuration to new "Accounts" tab
+3. [](#bugfix)
+   * Fixed `'mbstring' extension is not loaded` error, use Polyfill instead [#3504](https://github.com/getgrav/grav/pull/3504)
+   * Fixed new `Utils::pathinfo()` and `Utils::basename()` being too strict for legacy use [#3542](https://github.com/getgrav/grav/issues/3542)
+   * Fixed non-standard video html atributes generated by `{{ media.html() }}` [#3540](https://github.com/getgrav/grav/issues/3540)
+   * Fixed entity sanitization for XSS detection
+   * Fixed avatar save location when `account://` stream points to custom directory
+   * Fixed bug in `Utils::url()` when path contains part of root
+
+# v1.7.30
+## 02/07/2022
+
+1. [](#new)
+    * Added twig filter `|field_parent` to get parent field name
+2. [](#bugfix)
+    * Fixed error while deleting retina image in admin
+    * Fixed "Page Authors" field in Security tab, wrongly loading and saving the value [#3525](https://github.com/getgrav/grav/issues/3525)
+    * Fixed accounts filter only matches against email address [getgrav/grav-plugin-admin#2224](https://github.com/getgrav/grav-plugin-admin/issues/2224)
+
+# v1.7.29.1
+## 01/31/2022
+
+1. [](#bugfix)
+    * Fixed `Call to undefined method` error when upgrading from Grav 1.6 [#3523](https://github.com/getgrav/grav/issues/3523)
+
+# v1.7.29
+## 01/28/2022
+
+1. [](#new)
+    * Added support for registering assets from `HtmlBlock`
+    * Added unicode-safe `Utils::basename()` and `Utils::pathinfo()` methods
+2. [](#improved)
+    * Improved `Filesystem::basename()` and `Filesystem::pathinfo()` to be unicode-safe
+    * Made path handling unicode-safe, use new `Utils::basename()` and `Utils::pathinfo()` everywhere
+3. [](#bugfix)
+    * Fixed error on thumbnail image creation
+    * Fixed MimeType for `gzip` (`application/x-gzip`)
+
+# v1.7.28
+## 01/24/2022
+
+1. [](#new)
+    * Added links and modules support to `HtmlBlock` class
+    * Added module support for twig script tag: `{% script module 'theme://js/module.mjs' %}`
+    * Added twig tag for links: `{% link icon 'theme://images/favicon.png' priority: 20 with { type: 'image/png' } %}`
+    * Added `HtmlBlock` support for `{% style %}`, `{% script %}` and `{% link %}` tags
+    * Support for page-level `redirect_default_route` frontmatter header override
+3. [](#bugfix)
+    * Fixed XSS check not detecting escaped `&#58`
+
+# v1.7.27.1
+## 01/12/2022
+
+3. [](#bugfix)
+   * Fixed a typo in CSS Asset pipeline that was erroneously joining files with `;`
+
+# v1.7.27
+## 01/12/2022
+
+1. [](#new)
+   * Support for `YubiKey OTP` 2-Factor authenticator
+   * Added support for generic `assets.link()` for external references. No pipeline support
+   * Added support for `assets.addJsModule()` with full pipeline support
+   * Added `Utils::getExtensionsByMime()` method to get all the registered extensions for the specific mime type
+   * Added `Media::getRoute()` and `Media::getRawRoute()` methods to get page route if available
+   * Added `Medium::getAlternatives()` to be able to list all the retina sizes
+2. [](#improved)
+   * Improved `Utils::download()` method to allow overrides on download name, mime and expires header
+   * Improved `onPageFallBackUrl` event
+   * Reorganized the Asset system configuration blueprint for clarity
+3. [](#bugfix)
+   * Fixed CLI `--env` and `--lang` options having no effect if they aren't added before all the other options
+   * Fixed scaled image medium filename when using non-existing retina file
+   * Fixed an issue with JS `imports` and pipelining Assets
+
+# v1.7.26.1
+## 01/04/2022
+
+3. [](#bugfix)
+   * Fixed `UserObject::getAccess()` after cloning the object
+
+# v1.7.26
+## 01/03/2022
+
+1. [](#new)
+    * Made `Grav::redirect()` to accept `Route` class
+    * Added `translated()` method to `PageTranslateInterface`
+    * Added second parameter to `UserObject::isMyself()` method
+    * Added `UserObject::$isAuthorizedCallable` to allow `$user->isAuthorized()` customization
+    * Use secure session cookies in HTTPS by default (`system.session.secure_https: true`)
+    * Added new `Plugin::inheritedConfigOption()` function to access plugin specific functions for page overrides
+2. [](#improved)
+   * Upgraded vendor libs for PHP 8.1 compatibility
+   * Upgraded to **composer v2.1.14** for PHP 8.1 compatibility
+   * Added third `$name` parameter to `Blueprint::flattenData()` method, useful for flattening repeating data
+   * `ControllerResponseTrait`: Redirect response should be json if the extension is .json
+   * When symlinking Grav install, include also tests
+   * Updated copyright year to `2022`
+3. [](#bugfix)
+   * Fixed bad key lookup in `FlexRelatedDirectoryTrait::getCollectionByProperty()`
+   * Fixed RequestHandlers `NotFoundException` having empty request
+   * Block `.json` files in web server configs
+   * Disabled pretty debug info for Flex as it slows down Twig rendering
+   * Fixed Twig being very slow when template overrides do not exist
+   * Fixed `UserObject::$authorizeCallable` binding to the user object
+   * Fixed `FlexIndex::call()` to return null instead of failing to call undefined method
+   * Fixed Flex directory configuration creating environment configuration when it should not
+
+# v1.7.25
+## 11/16/2021
+
+1. [](#new)
+    * Updated phpstan to v1.0
+    * Added `FlexObject::getDiff()` to see difference to the saved object
+2. [](#improved)
+    * Use Symfony `dump` instead of PHP's `vardump` in side the `{{ vardump(x) }}` Twig vardump function
+    * Added `route` and `request` to `onPagesInitialized` event
+    * Improved page cloning, added method `Page::initialize()`
+    * Improved `FlexObject::getChanges()`: return changed lists and arrays as whole instead of just changed keys/values
+    * Improved form validation JSON responses to contain list of failed fields with their error messages
+    * Improved redirects: send redirect response in JSON if the request was in JSON
+3. [](#bugfix)
+    * Fixed path traversal vulnerability when using `bin/grav server`
+    * Fixed unescaped error messages in JSON error responses
+    * Fixed `|t(variable)` twig filter in admin
+    * Fixed `FlexObject::getChanges()` always returning empty array
+    * Fixed form validation exceptions to use `400 Bad Request` instead of `500 Internal Server Error`
+
+# v1.7.24
+## 10/26/2021
+
+1. [](#new)
+    * Added support for image watermarks
+    * Added support to disable a form, making it readonly
+2. [](#improved)
+    * Flex `$user->authorize()` now checks user groups before `admin.super`, allowing deny rules to work properly
+3. [](#bugfix)
+    * Fixed a bug in `PermissionsReader` in PHP 7.3
+    * Fixed `session_store_active` language option (#3464)
+    * Fixed deprecated warnings on `ArrayAccess` in PHP 8.1
+    * Fixed XSS detection with `&colon;`
+
+# 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
+    * Added `UserObject::isMyself()` method to check if flex user is currently logged in
+    * Added support for custom form field options validation with `validate: options: key|ignore`
+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
 

+ 12 - 6
README.md

@@ -1,20 +1,19 @@
 # ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
 
 [![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
-[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
 [![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org)
- [![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors)
+ [![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/tiers/backers/badge.svg?label=Backers&color=brightgreen)](#backers) [![OpenCollective](https://opencollective.com/grav/tiers/supporters/badge.svg?label=Supporters&color=brightgreen)](#supporters) [![OpenCollective](https://opencollective.com/grav/tiers/sponsors/badge.svg?label=Sponsors&color=brightgreen)](#sponsors)
 
 Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform.  There is **Zero** installation required.  Just extract the ZIP archive, and you are already up and running.  It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
 
 The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
 
-* [Twig Templating](https://twig.sensiolabs.org/): for powerful control of the user interface
+* [Twig Templating](https://twig.symfony.com/): for powerful control of the user interface
 * [Markdown](https://en.wikipedia.org/wiki/Markdown): for easy content creation
 * [YAML](https://yaml.org): for simple configuration
 * [Parsedown](https://parsedown.org/): for fast Markdown and Markdown Extra support
 * [Doctrine Cache](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html): layer for performance
-* [Pimple Dependency Injection Container](https://pimple.sensiolabs.org/): for extensibility and maintainability
+* [Pimple Dependency Injection Container](https://github.com/silexphp/Pimple): for extensibility and maintainability
 * [Symfony Event Dispatcher](https://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling
 * [Symfony Console](https://symfony.com/doc/current/components/console/introduction.html): for CLI interface
 * [Gregwar Image Library](https://github.com/Gregwar/Image): for dynamic image manipulation
@@ -118,12 +117,19 @@ If you discover a possible security issue related to Grav or one of its plugins,
 * More [Awesome Grav Stuff](https://github.com/getgrav/awesome-grav)
 
 # Backers
-Support Grav with a monthly donation to help us continue development. [[Become a backer](https://opencollective.com/grav#backer)]
+Support Grav with a monthly donation to help us continue development. [[Become a backer](https://opencollective.com/grav/contribute)]
 
 <img src="https://opencollective.com/grav/tiers/backers.svg?avatarHeight=36&width=600" />
 
+
+# Supporters
+Support Grav with a monthly donation to help us continue development. [[Become a supporter](https://opencollective.com/grav/contribute)]
+
+<img src="https://opencollective.com/grav/tiers/supporters.svg?avatarHeight=36&width=600" />
+
+
 # Sponsors
-Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/grav#sponsor)]
+Support Grav with a yearly donation to help us continue development. [[Become a sponsor](https://opencollective.com/grav/contribute)]
 
 <img src="https://opencollective.com/grav/tiers/sponsors.svg?avatarHeight=36&width=600" />
 

+ 1 - 1
assets/.gitkeep

@@ -1 +1 @@
-/* @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved. */
+/* @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved. */

BIN
bin/composer.phar


+ 3 - 11
bin/gpm

@@ -2,7 +2,7 @@
 <?php
 
 /**
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -25,18 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
 
 $autoload = require __DIR__ . '/../vendor/autoload.php';
 
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
-}
-
-if (!ini_get('date.timezone')) {
-    date_default_timezone_set('UTC');
-}
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
 
 // Set internal encoding.
-if (!\extension_loaded('mbstring')) {
-    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
-}
 @ini_set('default_charset', 'UTF-8');
 mb_internal_encoding('UTF-8');
 

+ 3 - 11
bin/grav

@@ -2,7 +2,7 @@
 <?php
 
 /**
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -25,18 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
 
 $autoload = require __DIR__ . '/../vendor/autoload.php';
 
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
-}
-
-if (!ini_get('date.timezone')) {
-    date_default_timezone_set('UTC');
-}
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
 
 // Set internal encoding.
-if (!\extension_loaded('mbstring')) {
-    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
-}
 @ini_set('default_charset', 'UTF-8');
 mb_internal_encoding('UTF-8');
 

+ 3 - 11
bin/plugin

@@ -2,7 +2,7 @@
 <?php
 
 /**
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -25,18 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
 
 $autoload = require __DIR__ . '/../vendor/autoload.php';
 
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
-}
-
-if (!ini_get('date.timezone')) {
-    date_default_timezone_set('UTC');
-}
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
 
 // Set internal encoding.
-if (!\extension_loaded('mbstring')) {
-    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
-}
 @ini_set('default_charset', 'UTF-8');
 mb_internal_encoding('UTF-8');
 

+ 28 - 18
composer.json

@@ -19,17 +19,19 @@
         "ext-zip": "*",
         "ext-dom": "*",
         "ext-libxml": "*",
-        "symfony/polyfill-mbstring": "~1.20",
-        "symfony/polyfill-iconv": "^1.20",
-        "symfony/polyfill-php74": "^1.20",
-        "symfony/polyfill-php80": "^1.20",
+        "ext-gd": "*",
+        "symfony/polyfill-mbstring": "~1.23",
+        "symfony/polyfill-iconv": "^1.23",
+        "symfony/polyfill-php74": "^1.23",
+        "symfony/polyfill-php80": "^1.23",
+        "symfony/polyfill-php81": "^1.23",
         "psr/simple-cache": "^1.0",
         "psr/http-message": "^1.0",
         "psr/http-server-middleware": "^1.0",
-        "psr/container": "~1.0.0",
+        "psr/container": "~1.1.0",
         "nyholm/psr7-server": "^1.0",
         "nyholm/psr7": "^1.3",
-        "twig/twig": "~1.44",
+        "twig/twig": "~v1.44",
         "erusev/parsedown": "^1.7",
         "erusev/parsedown-extra": "~0.8",
         "symfony/contracts": "~1.1",
@@ -47,25 +49,24 @@
         "getgrav/image": "^3.0",
         "getgrav/cache": "^2.0",
         "donatj/phpuseragentparser": "~1.1",
-        "pimple/pimple": "~3.3.0",
+        "pimple/pimple": "~3.5.0",
         "rockettheme/toolbox": "~1.5",
         "maximebf/debugbar": "~1.16",
         "league/climate": "^3.6",
-        "antoligy/dom-string-iterators": "^1.0",
         "miljar/php-exif": "^0.6",
         "composer/ca-bundle": "^1.2",
         "dragonmantank/cron-expression": "^1.2",
-        "phive/twig-extensions-deferred": "^1.0",
         "willdurand/negotiation": "^3.0",
         "itsgoingd/clockwork": "^5.0",
-        "enshrined/svg-sanitize": "~0.13",
         "symfony/http-client": "^4.4",
-        "composer/semver": "^1.4"
+        "composer/semver": "^1.4",
+        "rhukster/dom-sanitizer": "^1.0",
+        "multiavatar/multiavatar-php": "^1.0"
     },
     "require-dev": {
         "codeception/codeception": "^4.1",
-        "phpstan/phpstan": "^0.12",
-        "phpstan/phpstan-deprecation-rules": "^0.12",
+        "phpstan/phpstan": "^1.8",
+        "phpstan/phpstan-deprecation-rules": "^1.0",
         "phpunit/php-code-coverage": "~9.2",
         "getgrav/markdowndocs": "^2.0",
         "codeception/module-asserts": "^1.3",
@@ -83,7 +84,8 @@
         "ext-intl": "Recommended for multi-language sites",
         "ext-memcache": "Needed to support Memcache servers",
         "ext-memcached": "Needed to support Memcached servers",
-        "ext-redis": "Needed to support Redis servers"
+        "ext-redis": "Needed to support Redis servers",
+        "ext-exif": "Needed to use exif data from images."
     },
     "config": {
         "apcu-autoloader": true,
@@ -93,12 +95,20 @@
     },
     "autoload": {
         "psr-4": {
-            "Grav\\": "system/src/Grav"
+            "Grav\\": "system/src/Grav",
+            "Twig\\": "system/src/Twig"
         },
         "files": [
-            "system/defines.php"
+            "system/defines.php",
+            "system/src/DOMLettersIterator.php",
+            "system/src/DOMWordsIterator.php"
         ]
     },
+    "autoload-dev": {
+        "psr-4": {
+            "PHPStan\\": "tests/phpstan/classes"
+        }
+    },
     "archive": {
         "exclude": [
             "VERSION"
@@ -107,8 +117,8 @@
     "scripts": {
         "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
         "post-create-project-cmd": "bin/grav install",
-        "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src",
-        "phpstan-framework": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
+        "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon --memory-limit=720M system/src",
+        "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
         "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins",
         "test": "vendor/bin/codecept run unit",
         "test-windows": "vendor\\bin\\codecept run unit"

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


+ 5 - 5
fixperms.sh

@@ -1,8 +1,8 @@
 #!/bin/sh
 chgrp www-data .
 chgrp -R 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
+sh -c "find . -type f | xargs chmod 664"
+sh -c "find ./bin -type f | xargs chmod 775"
+sh -c "find . -type d | xargs chmod 775"
+sh -c "find . -type d | xargs chmod +s"
+sh -c "umask 0002"

+ 12 - 26
index.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav.Core
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -12,28 +12,14 @@ namespace Grav;
 \define('GRAV_REQUEST_TIME', microtime(true));
 \define('GRAV_PHP_MIN', '7.3.6');
 
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
-}
-
 if (PHP_SAPI === 'cli-server') {
-    $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony
-') !== false;
+    $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
+
     if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
         die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
     }
 }
 
-// Set timezone to default, falls back to system if php.ini not set
-date_default_timezone_set(@date_default_timezone_get());
-
-// Set internal encoding.
-if (!\extension_loaded('mbstring')) {
-    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
-}
-@ini_set('default_charset', 'UTF-8');
-mb_internal_encoding('UTF-8');
-
 // Ensure vendor libraries exist
 $autoload = __DIR__ . '/vendor/autoload.php';
 if (!is_file($autoload)) {
@@ -43,23 +29,23 @@ if (!is_file($autoload)) {
 // Register the auto-loader.
 $loader = require $autoload;
 
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
+
+// Set internal encoding.
+@ini_set('default_charset', 'UTF-8');
+mb_internal_encoding('UTF-8');
+
 use Grav\Common\Grav;
 use RocketTheme\Toolbox\Event\Event;
 
 // Get the Grav instance
-$grav = Grav::instance(
-    array(
-        'loader' => $loader
-    )
-);
+$grav = Grav::instance(array('loader' => $loader));
 
 // Process the page
 try {
     $grav->process();
-} catch (\Error $e) {
-    $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
-    throw $e;
-} catch (\Exception $e) {
+} catch (\Error|\Exception $e) {
     $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
     throw $e;
 }

Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
system/assets/debugger/phpdebugbar.css


+ 2 - 1
system/blueprints/config/scheduler.yaml

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

+ 202 - 54
system/blueprints/config/system.yaml

@@ -448,6 +448,17 @@ form:
               validate:
                 type: bool
 
+            languages.debug:
+              type: toggle
+              label: PLUGIN_ADMIN.LANGUAGE_DEBUG
+              help: PLUGIN_ADMIN.LANGUAGE_DEBUG_HELP
+              highlight: 0
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              validate:
+                type: bool
+
         http_headers:
           type: tab
           title: PLUGIN_ADMIN.HTTP_HEADERS
@@ -608,7 +619,6 @@ form:
                 file: File
                 apc: APC
                 apcu: APCu
-                xcache: Xcache
                 memcache: Memcache
                 memcached: Memcached
                 wincache: WinCache
@@ -888,9 +898,45 @@ form:
           title: PLUGIN_ADMIN.ASSETS
 
           fields:
-            assets_section:
+            general_config_section:
+              type: section
+              title: PLUGIN_ADMIN.GENERAL_CONFIG
+              underline: true
+
+            assets.enable_asset_timestamp:
+              type: toggle
+              label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
+              help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
+              highlight: 0
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              validate:
+                type: bool
+
+            assets.enable_asset_sri:
+              type: toggle
+              label: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS
+              help: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS_HELP
+              highlight: 0
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              validate:
+                type: bool
+
+            assets.collections:
+              type: multilevel
+              label: PLUGIN_ADMIN.COLLECTIONS
+              placeholder_key: collection_name
+              placeholder_value: collection_path
+              validate:
+                type: array
+
+
+            css_assets_section:
               type: section
-              title: PLUGIN_ADMIN.ASSETS
+              title: PLUGIN_ADMIN.CSS_ASSETS
               underline: true
 
             assets.css_pipeline:
@@ -959,6 +1005,11 @@ form:
               validate:
                 type: bool
 
+            js_assets_section:
+              type: section
+              title: PLUGIN_ADMIN.JS_ASSETS
+              underline: true
+
             assets.js_pipeline:
               type: toggle
               label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
@@ -1003,10 +1054,15 @@ form:
               validate:
                 type: bool
 
-            assets.enable_asset_timestamp:
+            js_module_assets_section:
+              type: section
+              title: PLUGIN_ADMIN.JS_MODULE_ASSETS
+              underline: true
+
+            assets.js_module_pipeline:
               type: toggle
-              label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
-              help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
+              label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE
+              help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_HELP
               highlight: 0
               options:
                 1: PLUGIN_ADMIN.YES
@@ -1014,24 +1070,29 @@ form:
               validate:
                 type: bool
 
-            assets.enable_asset_sri:
+            assets.js_module_pipeline_include_externals:
               type: toggle
-              label: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS
-              help: PLUGIN_ADMIN.ENABLED_SRI_ON_ASSETS_HELP
-              highlight: 0
+              label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS
+              help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS_HELP
+              highlight: 1
               options:
                 1: PLUGIN_ADMIN.YES
                 0: PLUGIN_ADMIN.NO
               validate:
                 type: bool
 
-            assets.collections:
-              type: multilevel
-              label: PLUGIN_ADMIN.COLLECTIONS
-              placeholder_key: collection_name
-              placeholder_value: collection_path
+            assets.js_module_pipeline_before_excludes:
+              type: toggle
+              label: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES
+              help: PLUGIN_ADMIN.JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES_HELP
+              highlight: 1
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
               validate:
-                type: array
+                type: bool
+
+
 
         errors:
           type: tab
@@ -1105,6 +1166,13 @@ form:
                 local6: local6
                 local7: local7
 
+            log.syslog.tag:
+              type: text
+              size: small
+              label: PLUGIN_ADMIN.SYSLOG_TAG
+              help: PLUGIN_ADMIN.SYSLOG_TAG_HELP
+              placeholder: "grav"
+
         debugger:
           type: tab
           title: PLUGIN_ADMIN.DEBUGGER
@@ -1394,6 +1462,18 @@ form:
               validate:
                 type: bool
 
+            session.secure_https:
+              type: toggle
+              label: PLUGIN_ADMIN.SESSION_SECURE_HTTPS
+              help: PLUGIN_ADMIN.SESSION_SECURE_HTTPS_HELP
+              highlight: 1
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              default: true
+              validate:
+                type: bool
+
             session.httponly:
               type: toggle
               label: PLUGIN_ADMIN.SESSION_HTTPONLY
@@ -1446,6 +1526,10 @@ form:
               title: PLUGIN_ADMIN.ADVANCED
               underline: true
 
+            gpm_section:
+              type: section
+              title: PLUGIN_ADMIN.GPM_SECTION
+
             gpm.releases:
               type: toggle
               label: PLUGIN_ADMIN.GPM_RELEASES
@@ -1455,14 +1539,23 @@ form:
                 stable: PLUGIN_ADMIN.STABLE
                 testing: PLUGIN_ADMIN.TESTING
 
-            gpm.proxy_url:
-              type: text
-              size: medium
-              placeholder: "e.g. 127.0.0.1:3128"
-              label: PLUGIN_ADMIN.PROXY_URL
-              help: PLUGIN_ADMIN.PROXY_URL_HELP
+            gpm.official_gpm_only:
+              type: toggle
+              label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
+              highlight: 1
+              help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              default: true
+              validate:
+                type: bool
+
+            http_section:
+              type: section
+              title: PLUGIN_ADMIN.HTTP_SECTION
 
-            gpm.method:
+            http.method:
               type: toggle
               label: PLUGIN_ADMIN.GPM_METHOD
               highlight: auto
@@ -1472,29 +1565,66 @@ form:
                 fopen: PLUGIN_ADMIN.FOPEN
                 curl: PLUGIN_ADMIN.CURL
 
-            gpm.official_gpm_only:
+            http.enable_proxy:
               type: toggle
-              label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
+              label: PLUGIN_ADMIN.SSL_ENABLE_PROXY
               highlight: 1
-              help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
               options:
                 1: PLUGIN_ADMIN.YES
                 0: PLUGIN_ADMIN.NO
-              default: true
+              default: false
               validate:
                 type: bool
 
-            gpm.verify_peer:
+            http.proxy_url:
+              type: text
+              size: medium
+              placeholder: "e.g. 127.0.0.1:3128"
+              label: PLUGIN_ADMIN.PROXY_URL
+              help: PLUGIN_ADMIN.PROXY_URL_HELP
+
+            http.proxy_cert_path:
+              type: text
+              size: medium
+              placeholder: "e.g. /Users/bob/certs/"
+              label: PLUGIN_ADMIN.PROXY_CERT
+              help: PLUGIN_ADMIN.PROXY_CERT_HELP
+
+            http.verify_peer:
               type: toggle
-              label: PLUGIN_ADMIN.GPM_VERIFY_PEER
+              label: PLUGIN_ADMIN.SSL_VERIFY_PEER
               highlight: 1
-              help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
+              help: PLUGIN_ADMIN.SSL_VERIFY_PEER_HELP
               options:
                 1: PLUGIN_ADMIN.YES
                 0: PLUGIN_ADMIN.NO
               validate:
                 type: bool
 
+            http.verify_host:
+              type: toggle
+              label: PLUGIN_ADMIN.SSL_VERIFY_HOST
+              highlight: 1
+              help: PLUGIN_ADMIN.SSL_VERIFY_HOST_HELP
+              options:
+                1: PLUGIN_ADMIN.YES
+                0: PLUGIN_ADMIN.NO
+              validate:
+                type: bool
+
+            http.concurrent_connections:
+              type: number
+              size: x-small
+              label: PLUGIN_ADMIN.HTTP_CONNECTIONS
+              help: PLUGIN_ADMIN.HTTP_CONNECTIONS_HELP
+              validate:
+                min: 1
+                max: 20
+
+            misc_section:
+              type: section
+              title: PLUGIN_ADMIN.MISC_SECTION
+
             reverse_proxy_setup:
               type: toggle
               label: PLUGIN_ADMIN.REVERSE_PROXY
@@ -1673,35 +1803,15 @@ form:
               validate:
                 type: bool
 
-        experimental:
+
+        accounts:
           type: tab
-          title: PLUGIN_ADMIN.EXPERIMENTAL
+          title: PLUGIN_ADMIN.ACCOUNTS
 
           fields:
-            experimental_section:
-              type: section
-              title: PLUGIN_ADMIN.EXPERIMENTAL
-              underline: true
-
-#            flex_pages:
-#              type: section
-#              title: Flex Pages
-#
-#            pages.type:
-#              type: select
-#              label: PLUGIN_ADMIN.PAGES_TYPE
-#              highlight: regular
-#              help: PLUGIN_ADMIN.PAGES_TYPE_HELP
-#              options:
-#                regular: PLUGIN_ADMIN.REGULAR
-#                flex: PLUGIN_ADMIN.FLEX
-
-            pages.type:
-              type: hidden
-
             flex_accounts:
               type: section
-              title: Flex Accounts
+              title: User Accounts
 
             accounts.type:
               type: select
@@ -1720,3 +1830,41 @@ form:
               options:
                 file: PLUGIN_ADMIN.FILE
                 folder: PLUGIN_ADMIN.FOLDER
+
+            accounts.avatar:
+              type: select
+              label: PLUGIN_ADMIN.AVATAR
+              default: gravatar
+              help: PLUGIN_ADMIN.AVATAR_HELP
+              options:
+                multiavatar: Multiavatar [local]
+                gravatar: Gravatar [external]
+
+#        experimental:
+#          type: tab
+#          title: PLUGIN_ADMIN.EXPERIMENTAL
+#
+#          fields:
+#            experimental_section:
+#              type: section
+#              title: PLUGIN_ADMIN.EXPERIMENTAL
+#              underline: true
+#
+#            flex_pages:
+#              type: section
+#              title: Flex Pages
+#
+#            pages.type:
+#              type: select
+#              label: PLUGIN_ADMIN.PAGES_TYPE
+#              highlight: regular
+#              help: PLUGIN_ADMIN.PAGES_TYPE_HELP
+#              options:
+#                regular: PLUGIN_ADMIN.REGULAR
+#                flex: PLUGIN_ADMIN.FLEX
+#
+#            pages.type:
+#              type: hidden
+
+
+

+ 2 - 2
system/blueprints/flex/pages.yaml

@@ -104,7 +104,7 @@ config:
 
     edit:
       title:
-        template: "{% if object.root %}Root <small>( &lt;root&gt; ){% else %}{{ (form.value('header.title') ?? form.value('folder'))|e }} <small>( {{ (object.getRoute().toString(false) ?: '/')|e }} )</small>{% endif %}"
+        template: "{% if object.root %}Root <small>( &lt;root&gt; )</small>{% else %}{{ (form.value('header.title') ?? form.value('folder'))|e }} <small>( {{ (object.getRoute().toString(false) ?: '/')|e }} )</small>{% endif %}"
 
       # TODO: not used yet
       buttons:
@@ -184,9 +184,9 @@ config:
       # Fields to be searched
       fields:
         - key
+        - slug
         - menu
         - title
-        - name
 
 blueprints:
   configure:

+ 13 - 0
system/blueprints/flex/user-accounts.yaml

@@ -122,6 +122,19 @@ config:
       fields:
         - key
         - email
+        - username
+        - fullname
+
+  relationships:
+    media:
+      type: media
+      cardinality: to-many
+    avatar:
+      type: media
+      cardinality: to-one
+#    roles:
+#      type: user-groups
+#      cardinality: to-many
 
 blueprints:
   configure:

+ 1 - 0
system/blueprints/flex/user-groups.yaml

@@ -113,6 +113,7 @@ config:
       fields:
         - key
         - groupname
+        - readableName
         - description
 
 blueprints:

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

@@ -320,6 +320,18 @@ form:
 
               fields:
 
+                header.redirect_default_route:
+                  type: toggle
+                  toggleable: true
+                  label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
+                  help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
+                  config-highlight@: system.pages.redirect_default_route
+                  options:
+                    1: PLUGIN_ADMIN.YES
+                    0: PLUGIN_ADMIN.NO
+                  validate:
+                    type: bool
+
                 header.routes.default:
                   type: text
                   toggleable: true

+ 9 - 9
system/blueprints/pages/external.yaml

@@ -1,7 +1,7 @@
-title: PLUGIN_ADMIN:EXTERNAL
+title: PLUGIN_ADMIN.EXTERNAL
 extends@:
-    type: default
-    context: blueprints://pages
+  type: default
+  context: blueprints://pages
 
 form:
   validation: loose
@@ -29,16 +29,16 @@ form:
               unset@: true
 
             header.external_url:
-                type: text
-                label: PLUGIN_ADMIN.EXTERNAL_URL
-                placeholder: https://getgrav.org
-                validate:
-                    required: true
+              type: text
+              label: PLUGIN_ADMIN.EXTERNAL_URL
+              placeholder: https://getgrav.org
+              validate:
+                required: true
+
         options:
           fields:
 
             publishing:
-
               fields:
 
                 header.date:

+ 3 - 7
system/blueprints/pages/partials/security.yaml

@@ -51,17 +51,13 @@ form:
             type: bool
 
         header.permissions.authors:
-          type: list
+          type: array
           toggleable: true
+          value_only: true
+          placeholder_value: PLUGIN_ADMIN.USERNAME
           label: PLUGIN_ADMIN.PAGE_AUTHORS
           help: PLUGIN_ADMIN.PAGE_AUTHORS_HELP
 
-          fields:
-            value:
-              type: text
-              placeholder: PLUGIN_ADMIN.USERNAME
-              style: vertical
-
         header.permissions.groups:
           ignore@: true
           type: acl_picker

+ 19 - 2
system/blueprints/user/account.yaml

@@ -11,10 +11,21 @@ form:
         avatar:
             type: file
             size: large
-            destination: 'user://accounts/avatars'
+            destination: 'account://avatars'
             multiple: false
             random_name: true
 
+        multiavatar_only:
+          type: conditional
+          condition: config.system.accounts.avatar == 'multiavatar'
+          fields:
+            avatar_hash:
+                type: text
+                label: ''
+                placeholder: 'e.g. dceaadcfda491f4e45'
+                description: PLUGIN_ADMIN.AVATAR_HASH
+                size: large
+
         content:
             type: section
             title: PLUGIN_ADMIN.ACCOUNT
@@ -107,6 +118,12 @@ form:
                     label: PLUGIN_ADMIN.2FA_SECRET
                     sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
 
+                yubikey_id:
+                    type: text
+                    label: PLUGIN_ADMIN.YUBIKEY_ID
+                    description: PLUGIN_ADMIN.YUBIKEY_HELP
+                    size: small
+                    maxlength: 12
 
 
 
@@ -123,7 +140,7 @@ form:
                     multiple: true
                     size: large
                     label: PLUGIN_ADMIN.GROUPS
-                    data-options@: '\Grav\Common\User\Group::groupNames'
+                    data-options@: 'Grav\Common\Flex\Types\UserGroups\UserGroupObject::groupNames'
                     classes: fancy
                     help: PLUGIN_ADMIN.GROUPS_HELP
                     validate:

+ 7 - 3
system/config/media.yaml

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

+ 1986 - 0
system/config/mime.yaml

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

+ 25 - 5
system/config/system.yaml

@@ -28,6 +28,7 @@ languages:
   override_locale: false                         # Override the default or system locale with language specific one
   content_fallback: {}                           # Custom language fallbacks. eg: {fr: ['fr', 'en']}
   pages_fallback_only: false                     # DEPRECATED: Use `content_fallback` instead
+  debug: false                                   # Debug language detection
 
 home:
   alias: '/home'                                 # Default path for home, ie /
@@ -35,6 +36,7 @@ home:
 
 pages:
   type: regular                                  # EXPERIMENTAL: Page type: regular or flex
+  dirs: ['page://']                              # Advanced functionality, allows for multiple page paths
   theme: quark                                   # Default theme (defaults to "quark" theme)
   order:
     by: default                                  # Order pages by "default", "alpha" or "date"
@@ -96,7 +98,7 @@ cache:
   purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
   clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
   clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
-  clear_images_by_default: false                  # By default grav will include processed images in cache clear, this can be disabled
+  clear_images_by_default: false                 # By default grav does not include processed images in cache clear, this can be enabled
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   gzip: false                                    # GZip compress the page output
@@ -127,11 +129,14 @@ assets:                                          # Configuration for Assets Mana
   js_pipeline: false                             # The JS pipeline is the unification of multiple JS resources into one file
   js_pipeline_include_externals: true            # Include external URLs in the pipeline by default
   js_pipeline_before_excludes: true              # Render the pipeline before any excluded files
+  js_module_pipeline: false                      # The JS Module pipeline is the unification of multiple JS Module resources into one file
+  js_module_pipeline_include_externals: true     # Include external URLs in the pipeline by default
+  js_module_pipeline_before_excludes: true       # Render the pipeline before any excluded files
   js_minify: true                                # Minify the JS during pipelining
   enable_asset_timestamp: false                  # Enable asset timestamps
   enable_asset_sri: false                        # Enable asset SRI
   collections:
-    jquery: system://assets/jquery/jquery-2.x.min.js
+    jquery: system://assets/jquery/jquery-3.x.min.js
 
 errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
@@ -141,6 +146,7 @@ log:
   handler: file                                 # Log handler. Currently supported: file | syslog
   syslog:
     facility: local6                            # Syslog facilities output
+    tag: grav                                   # Syslog tag. Default: "grav".
 
 debugger:
   enabled: false                                 # Enable Grav debugger and following settings
@@ -162,6 +168,12 @@ images:
     retina_scale: 1                              # scale to adjust auto-sizes for better handling of HiDPI resolutions
   defaults:
     loading: auto                                # Let browser pick [auto|lazy|eager]
+  watermark:
+    image: 'system://images/watermark.png'       # Path to a watermark image
+    position_y: 'center'                         # top|center|bottom
+    position_x: 'center'                         # left|center|right
+    scale: 33                                    # percentage of watermark scale
+    watermark_all: false                         # automatically watermark all images
 
 media:
   enable_media_timestamp: false                  # Enable media timestamps
@@ -176,6 +188,7 @@ session:
   name: grav-site                                # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name
   uniqueness: path                               # Should sessions be `path` based or `security.salt` based
   secure: false                                  # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
+  secure_https: true                             # Set session secure on HTTPS but not on HTTP. Has no effect if you have `session.secure: true`. Set to false if your site jumps between HTTP and HTTPS.
   httponly: true                                 # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
   samesite: Lax                                  # Set session SameSite. Possible values are Lax, Strict and None. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
   split: true                                    # Sessions should be independent between site and plugins (such as admin)
@@ -184,14 +197,21 @@ session:
 
 gpm:
   releases: stable                               # Set to either 'stable' or 'testing'
-  proxy_url:                                     # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
-  method: 'auto'                                 # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
-  verify_peer: true                              # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
   official_gpm_only: true                        # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
 
+http:
+  method: auto                                   # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
+  enable_proxy: true                             # Enable proxy server configuration
+  proxy_url:                                     # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
+  proxy_cert_path:                               # Local path to proxy certificate folder containing pem files
+  concurrent_connections: 5                      # Concurrent HTTP connections when multiplexing
+  verify_peer: true                              # Enable/Disable SSL verification of peer certificates
+  verify_host: true                              # Enable/Disable SSL verification of host certificates
+
 accounts:
   type: regular                                  # EXPERIMENTAL: Account type: regular or flex
   storage: file                                  # EXPERIMENTAL: Flex storage type: file or folder
+  avatar: gravatar                               # Avatar generator [multiavatar|gravatar]
 
 flex:
   cache:

+ 5 - 2
system/defines.php

@@ -3,13 +3,13 @@
 /**
  * @package    Grav\Core
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.7.16');
+define('GRAV_VERSION', '1.7.42.3');
 define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
 define('GRAV_TESTING', false);
 
@@ -99,3 +99,6 @@ define('RAW_CONTENT', 1);
 define('TWIG_CONTENT', 2);
 define('TWIG_CONTENT_LIST', 3);
 define('TWIG_TEMPLATES', 4);
+
+// Filters
+define('GRAV_SANITIZE_STRING', 5001);

BIN
system/images/watermark.png


+ 1 - 1
system/install.php

@@ -2,7 +2,7 @@
 /**
  * @package    Grav\Core
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 11 - 0
system/languages/ar.yaml

@@ -51,6 +51,7 @@ GRAV:
     VALIDATION_FAIL: '<b>فشل التحقق من صحة:</b>'
     INVALID_INPUT: 'إدخال غير صحيح في'
     MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
+    XSS_ISSUES: "مشاكل XSS محتملة تم اكتشافها في حقل '%s' '"
   MONTHS_OF_THE_YEAR:
     - 'كانون الثاني'
     - 'شباط'
@@ -72,6 +73,8 @@ GRAV:
     - 'الجمعة'
     - 'السبت'
     - 'الأحد'
+  YES: "نعم"
+  NO: "لا"
   CRON:
     EVERY: كل
     EVERY_HOUR: كل ساعة
@@ -80,3 +83,11 @@ GRAV:
     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: عدد عناصر غير صالح.
+    ERROR4: تعبير غير معروف

+ 16 - 0
system/languages/ca.yaml

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

+ 6 - 6
system/languages/es.yaml

@@ -44,7 +44,7 @@ GRAV:
     WK: sem
     MO: mes
     YR: año
-    DEC: dic
+    DEC: déc
     SECOND_PLURAL: segundos
     MINUTE_PLURAL: minutos
     HOUR_PLURAL: horas
@@ -64,7 +64,7 @@ GRAV:
     VALIDATION_FAIL: '<b>Falló la validación: </b>'
     INVALID_INPUT: 'Dato inválido en: '
     MISSING_REQUIRED_FIELD: 'Falta el campo requerido: '
-    XSS_ISSUES: "Se detectaron problemas XSS potenciales en el campo '%s'"
+    XSS_ISSUES: "Se detectaron potenciales problemas XSS en el campo '%s'"
   MONTHS_OF_THE_YEAR:
     - 'Enero'
     - 'Febrero'
@@ -86,7 +86,7 @@ GRAV:
     - 'Viernes'
     - 'Sábado'
     - 'Domingo'
-  YES: "Si"
+  YES: "Sí"
   NO: "No"
   CRON:
     EVERY: cada
@@ -96,12 +96,12 @@ GRAV:
     EVERY_DAY_OF_MONTH: cada día del mes
     EVERY_MONTH: cada mes
     TEXT_PERIOD: Cada <b />
-    TEXT_MINS: ' a <b /> minuto(s) despues de la hora'
+    TEXT_MINS: ' a <b /> minuto(s) después de la hora'
     TEXT_TIME: ' a <b />:<b />'
     TEXT_DOW: ' en <b />'
     TEXT_MONTH: ' de<b />'
     TEXT_DOM: ' en<b />'
-    ERROR1: La etiqueta %s no está soportada!
-    ERROR2: El número de elementos es erroneo
+    ERROR1: La etiqueta %s no está soportada!'
+    ERROR2: El número de elementos es erróneo
     ERROR3: El jquery_element debería establecerse en la configuración del jqCron
     ERROR4: Expresión no reconocida

+ 9 - 5
system/languages/fr.yaml

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

+ 3 - 0
system/languages/gl.yaml

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

+ 81 - 31
system/languages/id.yaml

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

+ 147 - 0
system/languages/mn.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/pt.yaml

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

+ 9 - 0
system/languages/si.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/tr.yaml

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

+ 16 - 1
system/languages/zh-tw.yaml

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

+ 20 - 3
system/router.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Core
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -13,8 +13,25 @@ if (PHP_SAPI !== 'cli-server') {
 
 $_SERVER['PHP_CLI_ROUTER'] = true;
 
-if (is_file($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $_SERVER['SCRIPT_NAME'])) {
-    return false;
+$root = $_SERVER['DOCUMENT_ROOT'];
+$path = $_SERVER['SCRIPT_NAME'];
+if ($path !== '/index.php' && is_file($root . $path)) {
+    if (!(
+        // Block all direct access to files and folders beginning with a dot
+        strpos($path, '/.') !== false
+        // Block all direct access for these folders
+        || preg_match('`^/(\.git|cache|bin|logs|backup|webserver-configs|tests)/`ui', $path)
+        // Block access to specific file types for these system folders
+        || preg_match('`^/(system|vendor)/(.*)\.(txt|xml|md|html|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
+        // Block access to specific file types for these user folders
+        || preg_match('`^/(user)/(.*)\.(txt|md|json|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
+        // Block all direct access to .md files
+        || preg_match('`\.md$`ui', $path)
+        // Block access to specific files in the root folder
+        || preg_match('`^/(LICENSE\.txt|composer\.lock|composer\.json|\.htaccess)$`ui', $path)
+    )) {
+        return false;
+    }
 }
 
 $grav_index = 'index.php';

+ 165 - 0
system/src/DOMLettersIterator.php

@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * Iterates individual characters (Unicode codepoints) of DOM text and CDATA nodes
+ * while keeping track of their position in the document.
+ *
+ * Example:
+ *
+ *  $doc = new DOMDocument();
+ *  $doc->load('example.xml');
+ *  foreach(new DOMLettersIterator($doc) as $letter) echo $letter;
+ *
+ * NB: If you only need characters without their position
+ *     in the document, use DOMNode->textContent instead.
+ *
+ * @author porneL http://pornel.net
+ * @license Public Domain
+ * @url https://github.com/antoligy/dom-string-iterators
+ *
+ * @implements Iterator<int,string>
+ */
+final class DOMLettersIterator implements Iterator
+{
+    /** @var DOMElement */
+    private $start;
+    /** @var DOMElement|null */
+    private $current;
+    /** @var int */
+    private $offset = -1;
+    /** @var int|null */
+    private $key;
+    /** @var array<int,string>|null */
+    private $letters;
+
+    /**
+     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)
+     *
+     * @param DOMNode $el
+     */
+    public function __construct(DOMNode $el)
+    {
+        if ($el instanceof DOMDocument) {
+            $el = $el->documentElement;
+        }
+
+        if (!$el instanceof DOMElement) {
+            throw new InvalidArgumentException('Invalid arguments, expected DOMElement or DOMDocument');
+        }
+
+        $this->start = $el;
+    }
+
+    /**
+     * Returns position in text as DOMText node and character offset.
+     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).
+     * node may be NULL if iterator has finished.
+     *
+     * @return array
+     */
+    public function currentTextPosition(): array
+    {
+        return [$this->current, $this->offset];
+    }
+
+    /**
+     * Returns DOMElement that is currently being iterated or NULL if iterator has finished.
+     *
+     * @return DOMElement|null
+     */
+    public function currentElement(): ?DOMElement
+    {
+        return $this->current ? $this->current->parentNode : null;
+    }
+
+    // Implementation of Iterator interface
+
+    /**
+     * @return int|null
+     */
+    public function key(): ?int
+    {
+        return $this->key;
+    }
+
+    /**
+     * @return void
+     */
+    public function next(): void
+    {
+        if (null === $this->current) {
+            return;
+        }
+
+        if ($this->current->nodeType === XML_TEXT_NODE || $this->current->nodeType === XML_CDATA_SECTION_NODE) {
+            if ($this->offset === -1) {
+                preg_match_all('/./us', $this->current->textContent, $m);
+                $this->letters = $m[0];
+            }
+
+            $this->offset++;
+            $this->key++;
+            if ($this->letters && $this->offset < count($this->letters)) {
+                return;
+            }
+
+            $this->offset = -1;
+        }
+
+        while ($this->current->nodeType === XML_ELEMENT_NODE && $this->current->firstChild) {
+            $this->current = $this->current->firstChild;
+            if ($this->current->nodeType === XML_TEXT_NODE || $this->current->nodeType === XML_CDATA_SECTION_NODE) {
+                $this->next();
+                return;
+            }
+        }
+
+        while (!$this->current->nextSibling && $this->current->parentNode) {
+            $this->current = $this->current->parentNode;
+            if ($this->current === $this->start) {
+                $this->current = null;
+                return;
+            }
+        }
+
+        $this->current = $this->current->nextSibling;
+
+        $this->next();
+    }
+
+    /**
+     * Return the current element
+     * @link https://php.net/manual/en/iterator.current.php
+     *
+     * @return string|null
+     */
+    public function current(): ?string
+    {
+        return $this->letters ? $this->letters[$this->offset] : null;
+    }
+
+    /**
+     * Checks if current position is valid
+     * @link https://php.net/manual/en/iterator.valid.php
+     *
+     * @return bool
+     */
+    public function valid(): bool
+    {
+        return (bool)$this->current;
+    }
+
+    /**
+     * @return void
+     */
+    public function rewind(): void
+    {
+        $this->current = $this->start;
+        $this->offset = -1;
+        $this->key = 0;
+        $this->letters = [];
+
+        $this->next();
+    }
+}
+

+ 158 - 0
system/src/DOMWordsIterator.php

@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * Iterates individual words of DOM text and CDATA nodes
+ * while keeping track of their position in the document.
+ *
+ * Example:
+ *
+ *  $doc = new DOMDocument();
+ *  $doc->load('example.xml');
+ *  foreach(new DOMWordsIterator($doc) as $word) echo $word;
+ *
+ * @author pjgalbraith http://www.pjgalbraith.com
+ * @author porneL http://pornel.net (based on DOMLettersIterator available at http://pornel.net/source/domlettersiterator.php)
+ * @license Public Domain
+ * @url https://github.com/antoligy/dom-string-iterators
+ *
+ * @implements Iterator<int,string>
+ */
+
+final class DOMWordsIterator implements Iterator
+{
+    /** @var DOMElement */
+    private $start;
+    /** @var DOMElement|null */
+    private $current;
+    /** @var int */
+    private $offset = -1;
+    /** @var int|null */
+    private $key;
+    /** @var array<int,array<int,int|string>>|null */
+    private $words;
+
+    /**
+     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)
+     *
+     * @param DOMNode $el
+     */
+    public function __construct(DOMNode $el)
+    {
+        if ($el instanceof DOMDocument) {
+            $el = $el->documentElement;
+        }
+
+        if (!$el instanceof DOMElement) {
+            throw new InvalidArgumentException('Invalid arguments, expected DOMElement or DOMDocument');
+        }
+
+        $this->start = $el;
+    }
+
+    /**
+     * Returns position in text as DOMText node and character offset.
+     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).
+     * node may be NULL if iterator has finished.
+     *
+     * @return array
+     */
+    public function currentWordPosition(): array
+    {
+        return [$this->current, $this->offset, $this->words];
+    }
+
+    /**
+     * Returns DOMElement that is currently being iterated or NULL if iterator has finished.
+     *
+     * @return DOMElement|null
+     */
+    public function currentElement(): ?DOMElement
+    {
+        return $this->current ? $this->current->parentNode : null;
+    }
+
+    // Implementation of Iterator interface
+
+    /**
+     * Return the key of the current element
+     * @link https://php.net/manual/en/iterator.key.php
+     * @return int|null
+     */
+    public function key(): ?int
+    {
+        return $this->key;
+    }
+
+    /**
+     * @return void
+     */
+    public function next(): void
+    {
+        if (null === $this->current) {
+            return;
+        }
+
+        if ($this->current->nodeType === XML_TEXT_NODE || $this->current->nodeType === XML_CDATA_SECTION_NODE) {
+            if ($this->offset === -1) {
+                $this->words = preg_split("/[\n\r\t ]+/", $this->current->textContent, -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_OFFSET_CAPTURE) ?: [];
+            }
+            $this->offset++;
+
+            if ($this->words && $this->offset < count($this->words)) {
+                $this->key++;
+                return;
+            }
+            $this->offset = -1;
+        }
+
+        while ($this->current->nodeType === XML_ELEMENT_NODE && $this->current->firstChild) {
+            $this->current = $this->current->firstChild;
+            if ($this->current->nodeType === XML_TEXT_NODE || $this->current->nodeType === XML_CDATA_SECTION_NODE) {
+                $this->next();
+                return;
+            }
+        }
+
+        while (!$this->current->nextSibling && $this->current->parentNode) {
+            $this->current = $this->current->parentNode;
+            if ($this->current === $this->start) {
+                $this->current = null;
+                return;
+            }
+        }
+
+        $this->current = $this->current->nextSibling;
+
+        $this->next();
+    }
+
+    /**
+     * Return the current element
+     * @link https://php.net/manual/en/iterator.current.php
+     * @return string|null
+     */
+    public function current(): ?string
+    {
+        return $this->words ? (string)$this->words[$this->offset][0] : null;
+    }
+
+    /**
+     * Checks if current position is valid
+     * @link https://php.net/manual/en/iterator.valid.php
+     * @return bool
+     */
+    public function valid(): bool
+    {
+        return (bool)$this->current;
+    }
+
+    public function rewind(): void
+    {
+        $this->current = $this->start;
+        $this->offset = -1;
+        $this->key = 0;
+        $this->words = [];
+
+        $this->next();
+    }
+}

+ 179 - 24
system/src/Grav/Common/Assets.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -16,8 +16,8 @@ use Grav\Common\Assets\Traits\TestingAssetsTrait;
 use Grav\Common\Config\Config;
 use Grav\Framework\Object\PropertyObject;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function array_slice;
 use function call_user_func_array;
-use function count;
 use function func_get_args;
 use function is_array;
 
@@ -30,14 +30,21 @@ class Assets extends PropertyObject
     use TestingAssetsTrait;
     use LegacyAssetsTrait;
 
+    const LINK = 'link';
     const CSS = 'css';
     const JS = 'js';
+    const JS_MODULE = 'js_module';
+    const LINK_COLLECTION = 'assets_link';
     const CSS_COLLECTION = 'assets_css';
     const JS_COLLECTION = 'assets_js';
+    const JS_MODULE_COLLECTION = 'assets_js_module';
+    const LINK_TYPE = Assets\Link::class;
     const CSS_TYPE = Assets\Css::class;
     const JS_TYPE = Assets\Js::class;
+    const JS_MODULE_TYPE = Assets\JsModule::class;
     const INLINE_CSS_TYPE = Assets\InlineCss::class;
     const INLINE_JS_TYPE = Assets\InlineJs::class;
+    const INLINE_JS_MODULE_TYPE = Assets\InlineJsModule::class;
 
     /** @const Regex to match CSS and JavaScript files */
     const DEFAULT_REGEX = '/.\.(css|js)$/i';
@@ -48,15 +55,24 @@ class Assets extends PropertyObject
     /** @const Regex to match JavaScript files */
     const JS_REGEX = '/.\.js$/i';
 
+    /** @const Regex to match JavaScriptModyle files */
+    const JS_MODULE_REGEX = '/.\.mjs$/i';
+
     /** @var string */
     protected $assets_dir;
     /** @var string */
     protected $assets_url;
 
+    /** @var array */
+    protected $assets_link = [];
     /** @var array */
     protected $assets_css = [];
     /** @var array */
     protected $assets_js = [];
+    /** @var array  */
+    protected $assets_js_module = [];
+
+
 
     // Following variables come from the configuration:
     /** @var bool */
@@ -66,19 +82,17 @@ class Assets extends PropertyObject
     /** @var bool */
     protected $css_pipeline_before_excludes;
     /** @var bool */
-    protected $inlinecss_pipeline_include_externals;
-    /** @var bool */
-    protected $inlinecss_pipeline_before_excludes;
-    /** @var bool */
     protected $js_pipeline;
     /** @var bool */
     protected $js_pipeline_include_externals;
     /** @var bool */
     protected $js_pipeline_before_excludes;
     /** @var bool */
-    protected $inlinejs_pipeline_include_externals;
+    protected $js_module_pipeline;
     /** @var bool */
-    protected $inlinejs_pipeline_before_excludes;
+    protected $js_module_pipeline_include_externals;
+    /** @var bool */
+    protected $js_module_pipeline_before_excludes;
     /** @var array */
     protected $pipeline_options = [];
 
@@ -110,7 +124,7 @@ class Assets extends PropertyObject
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
         $this->assets_url = $locator->findResource('asset://', false);
 
         $this->config($asset_config);
@@ -160,14 +174,27 @@ class Assets extends PropertyObject
      */
     public function add($asset)
     {
+        if (!$asset) {
+            return $this;
+        }
+
         $args = func_get_args();
 
         // More than one asset
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                array_shift($args);
-                $args = array_merge([$a], $args);
-                call_user_func_array([$this, 'add'], $args);
+            foreach ($asset as $index => $location) {
+                $params = array_slice($args, 1);
+                if (is_array($location)) {
+                    $params = array_shift($params);
+                    if (is_numeric($params)) {
+                        $params = [ 'priority' => $params ];
+                    }
+                    $params = [array_replace_recursive([], $location, $params)];
+                    $location = $index;
+                }
+
+                $params = array_merge([$location], $params);
+                call_user_func_array([$this, 'add'], $params);
             }
         } elseif (isset($this->collections[$asset])) {
             array_shift($args);
@@ -175,7 +202,8 @@ class Assets extends PropertyObject
             call_user_func_array([$this, 'add'], $args);
         } else {
             // Get extension
-            $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
+            $path = parse_url($asset, PHP_URL_PATH);
+            $extension = $path ? Utils::pathinfo($path, PATHINFO_EXTENSION) : '';
 
             // JavaScript or CSS
             if ($extension !== '') {
@@ -184,6 +212,8 @@ class Assets extends PropertyObject
                     call_user_func_array([$this, 'addCss'], $args);
                 } elseif ($extension === 'js') {
                     call_user_func_array([$this, 'addJs'], $args);
+                } elseif ($extension === 'mjs') {
+                    call_user_func_array([$this, 'addJsModule'], $args);
                 }
             }
         }
@@ -201,14 +231,19 @@ class Assets extends PropertyObject
     protected function addType($collection, $type, $asset, $options)
     {
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                $this->addType($collection, $type, $a, $options);
+            foreach ($asset as $index => $location) {
+                $assetOptions = $options;
+                if (is_array($location)) {
+                    $assetOptions = array_replace_recursive([], $options, $location);
+                    $location = $index;
+                }
+                $this->addType($collection, $type, $location, $assetOptions);
             }
 
             return $this;
         }
 
-        if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) {
+        if ($this->isValidType($type) && isset($this->collections[$asset])) {
             $this->addType($collection, $type, $this->collections[$asset], $options);
             return $this;
         }
@@ -216,7 +251,9 @@ class Assets extends PropertyObject
         // If pipeline disabled, set to position if provided, else after
         if (isset($options['pipeline'])) {
             if ($options['pipeline'] === false) {
-                $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS : $this::CSS;
+
+                $exclude_type = $this->getBaseType($type);
+
                 $excludes = strtolower($exclude_type . '_pipeline_before_excludes');
                 if ($this->{$excludes}) {
                     $default = 'after';
@@ -231,7 +268,13 @@ class Assets extends PropertyObject
         }
 
         // Add timestamp
-        $options['timestamp'] = $this->timestamp;
+        $timestamp_override = $options['timestamp'] ?? true;
+
+        if (filter_var($timestamp_override, FILTER_VALIDATE_BOOLEAN)) {
+            $options['timestamp'] = $this->timestamp;
+        } else {
+            $options['timestamp'] = null;
+        }
 
         // Set order
         $group = $options['group'] ?? 'head';
@@ -255,6 +298,16 @@ class Assets extends PropertyObject
         return $this;
     }
 
+    /**
+     * Add a CSS asset or a collection of assets.
+     *
+     * @return $this
+     */
+    public function addLink($asset)
+    {
+        return $this->addType($this::LINK_COLLECTION, $this::LINK_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::LINK_TYPE));
+    }
+
     /**
      * Add a CSS asset or a collection of assets.
      *
@@ -295,6 +348,25 @@ class Assets extends PropertyObject
         return $this->addType($this::JS_COLLECTION, $this::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_TYPE));
     }
 
+        /**
+     * Add a JS asset or a collection of assets.
+     *
+     * @return $this
+     */
+    public function addJsModule($asset)
+    {
+        return $this->addType($this::JS_MODULE_COLLECTION, $this::JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_MODULE_TYPE));
+    }
+
+    /**
+     * Add an Inline JS asset or a collection of assets.
+     *
+     * @return $this
+     */
+    public function addInlineJsModule($asset)
+    {
+        return $this->addType($this::JS_MODULE_COLLECTION, $this::INLINE_JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_MODULE_TYPE));
+    }
 
     /**
      * Add/replace collection.
@@ -386,7 +458,7 @@ class Assets extends PropertyObject
         $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
 
         // Pipeline
-        if ($this->{$pipeline_enabled}) {
+        if ($this->{$pipeline_enabled} ?? false) {
             $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
 
             $pipeline = new Pipeline($options);
@@ -418,9 +490,29 @@ class Assets extends PropertyObject
      * @param  array  $attributes
      * @return string
      */
-    public function css($group = 'head', $attributes = [])
+    public function css($group = 'head', $attributes = [], $include_link = true)
+    {
+        $output = '';
+
+        if ($include_link) {
+            $output = $this->link($group, $attributes);
+        }
+
+        $output .= $this->render(self::CSS, $group, $attributes);
+
+        return $output;
+    }
+
+    /**
+     * Build the CSS link tags.
+     *
+     * @param  string $group name of the group
+     * @param  array  $attributes
+     * @return string
+     */
+    public function link($group = 'head', $attributes = [])
     {
-        return $this->render('css', $group, $attributes);
+        return $this->render(self::LINK, $group, $attributes);
     }
 
     /**
@@ -430,8 +522,71 @@ class Assets extends PropertyObject
      * @param  array  $attributes
      * @return string
      */
-    public function js($group = 'head', $attributes = [])
+    public function js($group = 'head', $attributes = [], $include_js_module = true)
+    {
+        $output = $this->render(self::JS, $group, $attributes);
+
+        if ($include_js_module) {
+            $output .= $this->jsModule($group, $attributes);
+        }
+
+        return $output;
+    }
+
+    /**
+     * Build the Javascript Modules tags
+     *
+     * @param string $group
+     * @param array $attributes
+     * @return string
+     */
+    public function jsModule($group = 'head', $attributes = [])
+    {
+        return $this->render(self::JS_MODULE, $group, $attributes);
+    }
+
+    /**
+     * @param string $group
+     * @param array $attributes
+     * @return string
+     */
+    public function all($group = 'head', $attributes = [])
+    {
+        $output = $this->css($group, $attributes, false);
+        $output .= $this->link($group, $attributes);
+        $output .= $this->js($group, $attributes, false);
+        $output .= $this->jsModule($group, $attributes);
+        return $output;
+    }
+
+    /**
+     * @param class-string $type
+     * @return bool
+     */
+    protected function isValidType($type)
+    {
+        return in_array($type, [self::CSS_TYPE, self::JS_TYPE, self::JS_MODULE_TYPE]);
+    }
+
+    /**
+     * @param class-string $type
+     * @return string
+     */
+    protected function getBaseType($type)
     {
-        return $this->render('js', $group, $attributes);
+        switch ($type) {
+            case $this::JS_TYPE:
+            case $this::INLINE_JS_TYPE:
+                $base_type = $this::JS;
+                break;
+            case $this::JS_MODULE_TYPE:
+            case $this::INLINE_JS_MODULE_TYPE:
+                $base_type = $this::JS_MODULE;
+                break;
+            default:
+                $base_type = $this::CSS;
+        }
+
+        return $base_type;
     }
 }

+ 24 - 5
system/src/Grav/Common/Assets/BaseAsset.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -26,8 +26,9 @@ abstract class BaseAsset extends PropertyObject
 {
     use AssetUtilsTrait;
 
-    protected const CSS_ASSET = true;
-    protected const JS_ASSET = false;
+    protected const CSS_ASSET = 1;
+    protected const JS_ASSET = 2;
+    protected const JS_MODULE_ASSET = 3;
 
     /** @var string|false */
     protected $asset;
@@ -69,7 +70,7 @@ abstract class BaseAsset extends PropertyObject
      * @param array $elements
      * @param string|null $key
      */
-    public function __construct(array $elements = [], $key = null)
+    public function __construct(array $elements = [], ?string $key = null)
     {
         $base_config = [
             'group' => 'head',
@@ -92,6 +93,10 @@ abstract class BaseAsset extends PropertyObject
      */
     public function init($asset, $options)
     {
+        if (!$asset) {
+            return false;
+        }
+
         $config = Grav::instance()['config'];
         $uri = Grav::instance()['uri'];
 
@@ -244,6 +249,7 @@ abstract class BaseAsset extends PropertyObject
      *
      * @return array
      */
+    #[\ReturnTypeWillChange]
     public function jsonSerialize()
     {
         return ['type' => $this->getType(), 'elements' => $this->getElements()];
@@ -259,6 +265,19 @@ abstract class BaseAsset extends PropertyObject
      */
     protected function cssRewrite($file, $dir, $local)
     {
-        return;
+        return '';
+    }
+
+    /**
+     * Finds relative JS urls() and rewrites the URL with an absolute one
+     *
+     * @param string $file the css source file
+     * @param string $dir local relative path to the css file
+     * @param bool $local is this a local or remote asset
+     * @return string
+     */
+    protected function jsRewrite($file, $dir, $local)
+    {
+        return '';
     }
 }

+ 207 - 0
system/src/Grav/Common/Assets/BlockAssets.php

@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Assets;
+use Grav\Common\Config\Config;
+use Grav\Common\Grav;
+use Grav\Framework\ContentBlock\HtmlBlock;
+use function strlen;
+
+/**
+ * Register block assets into Grav.
+ */
+class BlockAssets
+{
+    /**
+     * @param HtmlBlock $block
+     * @return void
+     */
+    public static function registerAssets(HtmlBlock $block): void
+    {
+        $grav = Grav::instance();
+
+        /** @var Assets $assets */
+        $assets = $grav['assets'];
+
+        $types = $block->getAssets();
+        foreach ($types as $type => $groups) {
+            switch ($type) {
+                case 'frameworks':
+                    static::registerFrameworks($assets, $groups);
+                    break;
+                case 'styles':
+                    static::registerStyles($assets, $groups);
+                    break;
+                case 'scripts':
+                    static::registerScripts($assets, $groups);
+                    break;
+                case 'links':
+                    static::registerLinks($assets, $groups);
+                    break;
+                case 'html':
+                    static::registerHtml($assets, $groups);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * @param Assets $assets
+     * @param array $list
+     * @return void
+     */
+    protected static function registerFrameworks(Assets $assets, array $list): void
+    {
+        if ($list) {
+            throw new \RuntimeException('Not Implemented');
+        }
+    }
+
+    /**
+     * @param Assets $assets
+     * @param array $groups
+     * @return void
+     */
+    protected static function registerStyles(Assets $assets, array $groups): void
+    {
+        $grav = Grav::instance();
+
+        /** @var Config $config */
+        $config = $grav['config'];
+
+        foreach ($groups as $group => $styles) {
+            foreach ($styles as $style) {
+                switch ($style[':type']) {
+                    case 'file':
+                        $options = [
+                            'priority' => $style[':priority'],
+                            'group' => $group,
+                            'type' => $style['type'],
+                            'media' => $style['media']
+                        ] + $style['element'];
+
+                        $assets->addCss(static::getRelativeUrl($style['href'], $config->get('system.assets.css_pipeline')), $options);
+                        break;
+                    case 'inline':
+                        $options = [
+                            'priority' => $style[':priority'],
+                            'group' => $group,
+                            'type' => $style['type'],
+                        ] + $style['element'];
+
+                        $assets->addInlineCss($style['content'], $options);
+                        break;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param Assets $assets
+     * @param array $groups
+     * @return void
+     */
+    protected static function registerScripts(Assets $assets, array $groups): void
+    {
+        $grav = Grav::instance();
+
+        /** @var Config $config */
+        $config = $grav['config'];
+
+        foreach ($groups as $group => $scripts) {
+            $group = $group === 'footer' ? 'bottom' : $group;
+
+            foreach ($scripts as $script) {
+                switch ($script[':type']) {
+                    case 'file':
+                        $options = [
+                            'group' => $group,
+                            'priority' => $script[':priority'],
+                            'src' => $script['src'],
+                            'type' => $script['type'],
+                            'loading' => $script['loading'],
+                            'defer' => $script['defer'],
+                            'async' => $script['async'],
+                            'handle' => $script['handle']
+                        ] + $script['element'];
+
+                        $assets->addJs(static::getRelativeUrl($script['src'], $config->get('system.assets.js_pipeline')), $options);
+                        break;
+                    case 'inline':
+                        $options = [
+                            'priority' => $script[':priority'],
+                            'group' => $group,
+                            'type' => $script['type'],
+                            'loading' => $script['loading']
+                        ] + $script['element'];
+
+                        $assets->addInlineJs($script['content'], $options);
+                        break;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param Assets $assets
+     * @param array $groups
+     * @return void
+     */
+    protected static function registerLinks(Assets $assets, array $groups): void
+    {
+        foreach ($groups as $group => $links) {
+            foreach ($links as $link) {
+                $href = $link['href'];
+                $options = [
+                    'group' => $group,
+                    'priority' => $link[':priority'],
+                    'rel' => $link['rel'],
+                ] + $link['element'];
+
+                $assets->addLink($href, $options);
+            }
+        }
+    }
+
+    /**
+     * @param Assets $assets
+     * @param array $groups
+     * @return void
+     */
+    protected static function registerHtml(Assets $assets, array $groups): void
+    {
+        if ($groups) {
+            throw new \RuntimeException('Not Implemented');
+        }
+    }
+
+    /**
+     * @param string $url
+     * @param bool $pipeline
+     * @return string
+     */
+    protected static function getRelativeUrl($url, $pipeline)
+    {
+        $grav = Grav::instance();
+
+        $base = rtrim($grav['base_url'], '/') ?: '/';
+
+        if (strpos($url, $base) === 0) {
+            if ($pipeline) {
+                // Remove file timestamp if CSS pipeline has been enabled.
+                $url = preg_replace('|[?#].*|', '', $url);
+            }
+
+            return substr($url, strlen($base) - 1);
+        }
+        return $url;
+    }
+}

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -22,7 +22,7 @@ class Css extends BaseAsset
      * @param array $elements
      * @param string|null $key
      */
-    public function __construct(array $elements = [], $key = null)
+    public function __construct(array $elements = [], ?string $key = null)
     {
         $base_options = [
             'asset_type' => 'css',

+ 2 - 2
system/src/Grav/Common/Assets/InlineCss.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -22,7 +22,7 @@ class InlineCss extends BaseAsset
      * @param array $elements
      * @param string|null $key
      */
-    public function __construct(array $elements = [], $key = null)
+    public function __construct(array $elements = [], ?string $key = null)
     {
         $base_options = [
             'asset_type' => 'css',

+ 2 - 2
system/src/Grav/Common/Assets/InlineJs.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -22,7 +22,7 @@ class InlineJs extends BaseAsset
      * @param array $elements
      * @param string|null $key
      */
-    public function __construct(array $elements = [], $key = null)
+    public function __construct(array $elements = [], ?string $key = null)
     {
         $base_options = [
             'asset_type' => 'js',

+ 46 - 0
system/src/Grav/Common/Assets/InlineJsModule.php

@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+/**
+ * Class InlineJs
+ * @package Grav\Common\Assets
+ */
+class InlineJsModule extends BaseAsset
+{
+    /**
+     * InlineJs constructor.
+     * @param array $elements
+     * @param string|null $key
+     */
+    public function __construct(array $elements = [], ?string $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js_module',
+            'attributes' => ['type' => 'module'],
+            'position' => 'after'
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    /**
+     * @return string
+     */
+    public function render()
+    {
+        return '<script' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</script>\n";
+    }
+
+}

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -22,7 +22,7 @@ class Js extends BaseAsset
      * @param array $elements
      * @param string|null $key
      */
-    public function __construct(array $elements = [], $key = null)
+    public function __construct(array $elements = [], ?string $key = null)
     {
         $base_options = [
             'asset_type' => 'js',

+ 49 - 0
system/src/Grav/Common/Assets/JsModule.php

@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+/**
+ * Class Js
+ * @package Grav\Common\Assets
+ */
+class JsModule extends BaseAsset
+{
+    /**
+     * Js constructor.
+     * @param array $elements
+     * @param string|null $key
+     */
+    public function __construct(array $elements = [], ?string $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js_module',
+            'attributes' => ['type' => 'module']
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+        /**
+     * @return string
+     */
+    public function render()
+    {
+        if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
+            $buffer = $this->gatherLinks([$this], self::JS_MODULE_ASSET);
+            return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
+        }
+
+        return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . "></script>\n";
+    }
+}

+ 43 - 0
system/src/Grav/Common/Assets/Link.php

@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+/**
+ * Class Link
+ * @package Grav\Common\Assets
+ */
+class Link extends BaseAsset
+{
+    /**
+     * Css constructor.
+     * @param array $elements
+     * @param string|null $key
+     */
+    public function __construct(array $elements = [], ?string $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'link',
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    /**
+     * @return string
+     */
+    public function render()
+    {
+        return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . ">\n";
+    }
+}

+ 77 - 19
system/src/Grav/Common/Assets/Pipeline.php

@@ -3,15 +3,15 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common\Assets;
 
-use Grav\Common\Assets\BaseAsset;
 use Grav\Common\Assets\Traits\AssetUtilsTrait;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
@@ -29,12 +29,16 @@ class Pipeline extends PropertyObject
 {
     use AssetUtilsTrait;
 
-    protected const CSS_ASSET = true;
-    protected const JS_ASSET = false;
+    protected const CSS_ASSET = 1;
+    protected const JS_ASSET = 2;
+    protected const JS_MODULE_ASSET = 3;
 
     /** @const Regex to match CSS urls */
     protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
 
+    /** @const Regex to match JS imports */
+    protected const JS_IMPORT_REGEX = '{import.+from\s?[\'|\"](.+?)[\'|\"]}';
+
     /** @const Regex to match CSS sourcemap comments */
     protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
 
@@ -88,7 +92,14 @@ class Pipeline extends PropertyObject
         $uri = Grav::instance()['uri'];
 
         $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
+        if (!$this->assets_dir) {
+            // Attempt to create assets folder if it doesn't exist yet.
+            $this->assets_dir = $locator->findResource('asset://', true, true);
+            Folder::mkdir($this->assets_dir);
+            $locator->clearCache();
+        }
+
         $this->assets_url = $locator->findResource('asset://', false);
     }
 
@@ -115,14 +126,13 @@ class Pipeline extends PropertyObject
 
         // Compute uid based on assets and timestamp
         $json_assets = json_encode($assets);
-        $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
+        $uid = md5($json_assets . (int)$this->css_minify . (int)$this->css_rewrite . $group);
         $file = $uid . '.css';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -141,7 +151,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 
@@ -163,7 +173,7 @@ class Pipeline extends PropertyObject
      * @param array $attributes
      * @return bool|string     URL or generated content if available, else false
      */
-    public function renderJs($assets, $group, $attributes = [])
+    public function renderJs($assets, $group, $attributes = [], $type = self::JS_ASSET)
     {
         // temporary list of assets to pipeline
         $inline_group = false;
@@ -182,10 +192,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.js';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -193,7 +202,7 @@ class Pipeline extends PropertyObject
             }
 
             // Concatenate files
-            $buffer = $this->gatherLinks($assets, self::JS_ASSET);
+            $buffer = $this->gatherLinks($assets, $type);
 
             // Minify if required
             if ($this->shouldMinify('js')) {
@@ -204,7 +213,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 
@@ -218,6 +227,19 @@ class Pipeline extends PropertyObject
         return $output;
     }
 
+        /**
+     * Minify and concatenate JS files.
+     *
+     * @param array $assets
+     * @param string $group
+     * @param array $attributes
+     * @return bool|string     URL or generated content if available, else false
+     */
+    public function renderJs_Module($assets, $group, $attributes = [])
+    {
+        $attributes['type'] = 'module';
+        return $this->renderJs($assets, $group, $attributes, self::JS_MODULE_ASSET);
+    }
 
     /**
      * Finds relative CSS urls() and rewrites the URL with an absolute one
@@ -249,7 +271,7 @@ class Pipeline extends PropertyObject
                 $old_url = ltrim($old_url, '/');
             }
 
-            $new_url = ($local ? $this->base_url: '') . $old_url;
+            $new_url = ($local ? $this->base_url : '') . $old_url;
 
             return str_replace($matches[2], $new_url, $matches[0]);
         }, $file);
@@ -257,6 +279,42 @@ class Pipeline extends PropertyObject
         return $file;
     }
 
+    /**
+     * Finds relative JS urls() and rewrites the URL with an absolute one
+     *
+     * @param string $file the css source file
+     * @param string $dir local relative path to the css file
+     * @param bool $local is this a local or remote asset
+     * @return string
+     */
+    protected function jsRewrite($file, $dir, $local)
+    {
+        // Find any js import elements, grab the URLs and calculate an absolute path
+        // Then replace the old url with the new one
+        $file = (string)preg_replace_callback(self::JS_IMPORT_REGEX, function ($matches) use ($dir, $local) {
+
+            $old_url = $matches[1];
+
+            // Ensure link is not rooted to web server, a data URL, or to a remote host
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || $this->isRemoteLink($old_url)) {
+                return $matches[0];
+            }
+
+            // clean leading /
+            $old_url = Utils::normalizePath($dir . '/' . $old_url);
+            $old_url = str_replace('/./', '/', $old_url);
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
+                $old_url = ltrim($old_url, '/');
+            }
+
+            $new_url = ($local ? $this->base_url : '') . $old_url;
+
+            return str_replace($matches[1], $new_url, $matches[0]);
+        }, $file);
+
+        return $file;
+    }
+
     /**
      * @param string $type
      * @return bool

+ 18 - 11
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets\Traits
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -62,15 +62,13 @@ trait AssetUtilsTrait
      * Download and concatenate the content of several links.
      *
      * @param  array $assets
-     * @param  bool $css
+     * @param  int $type
      * @return string
      */
-    protected function gatherLinks(array $assets, $css = true)
+    protected function gatherLinks(array $assets, int $type = self::CSS_ASSET): string
     {
         $buffer = '';
-
-
-        foreach ($assets as $id => $asset) {
+        foreach ($assets as $asset) {
             $local = true;
 
             $link = $asset->getAsset();
@@ -102,21 +100,25 @@ trait AssetUtilsTrait
             }
 
             // Double check last character being
-            if (!$css) {
+            if ($type === self::JS_ASSET || $type === self::JS_MODULE_ASSET) {
                 $file = rtrim($file, ' ;') . ';';
             }
 
             // If this is CSS + the file is local + rewrite enabled
-            if ($css && $this->css_rewrite) {
+            if ($type === self::CSS_ASSET && $this->css_rewrite) {
                 $file = $this->cssRewrite($file, $relative_dir, $local);
             }
 
+            if ($type === self::JS_MODULE_ASSET) {
+                $file = $this->jsRewrite($file, $relative_dir, $local);
+            }
+
             $file = rtrim($file) . PHP_EOL;
             $buffer .= $file;
         }
 
         // Pull out @imports and move to top
-        if ($css) {
+        if ($type === self::CSS_ASSET) {
             $buffer = $this->moveImports($buffer);
         }
 
@@ -135,7 +137,7 @@ trait AssetUtilsTrait
 
         $imports = [];
 
-        $file = (string)preg_replace_callback($regex, function ($matches) use (&$imports) {
+        $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) {
             $imports[] = $matches[0];
 
             return '';
@@ -156,6 +158,10 @@ trait AssetUtilsTrait
         $no_key = ['loading'];
 
         foreach ($this->attributes as $key => $value) {
+            if ($value === null) {
+                continue;
+            }
+
             if (is_numeric($key)) {
                 $key = $value;
             }
@@ -186,6 +192,7 @@ trait AssetUtilsTrait
         $querystring = '';
 
         $asset = $asset ?? $this->asset;
+        $attributes = $this->attributes;
 
         if (!empty($this->query)) {
             if (Utils::contains($asset, '?')) {
@@ -196,7 +203,7 @@ trait AssetUtilsTrait
         }
 
         if ($this->timestamp) {
-            if (Utils::contains($asset, '?') || $querystring) {
+            if ($querystring || Utils::contains($asset, '?')) {
                 $querystring .=  '&' . $this->timestamp;
             } else {
                 $querystring .= '?' . $this->timestamp;

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets\Traits
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets\Traits
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -285,6 +285,15 @@ trait TestingAssetsTrait
             return $this;
         }
 
+        // Add JavaScript Module files
+        if ($pattern === self::JS_MODULE_REGEX) {
+            foreach ($files as $file) {
+                $this->addJsModule($file);
+            }
+
+            return $this;
+        }
+
         // Unknown pattern.
         foreach ($files as $asset) {
             $this->add($asset);

+ 5 - 6
system/src/Grav/Common/Backup/Backups.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Backup
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -104,8 +104,8 @@ class Backups
      */
     public function getBackupDownloadUrl($backup, $base_url)
     {
-        $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
-        $download = urlencode(base64_encode(basename($backup)));
+        $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
+        $download = urlencode(base64_encode(Utils::basename($backup)));
         $url      = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim(
             $base_url,
             '/'
@@ -144,9 +144,8 @@ class Backups
     public static function getTotalBackupsSize()
     {
         $backups = static::getAvailableBackups();
-        $size = array_sum(array_column($backups, 'size'));
 
-        return $size ?? 0;
+        return $backups ? array_sum(array_column($backups, 'size')) : 0;
     }
 
     /**
@@ -222,7 +221,7 @@ class Backups
             $backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
         }
 
-        if (!file_exists($backup_root)) {
+        if (!$backup_root || !file_exists($backup_root)) {
             throw new RuntimeException("Backup location: {$backup_root} does not exist...");
         }
 

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 4 - 8
system/src/Grav/Common/Cache.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -141,7 +141,7 @@ class Cache extends Getters
         $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
 
         // Cache key allows us to invalidate all cache on configuration changes.
-        $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
+        $this->key = ($prefix ?: 'g') . '-' . $uniqueness;
         $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
         $this->driver_setting = $this->config->get('system.cache.driver');
         $this->driver = $this->getCacheDriver();
@@ -177,7 +177,7 @@ class Cache extends Getters
     public function purgeOldCache()
     {
         $cache_dir = dirname($this->cache_dir);
-        $current = basename($this->cache_dir);
+        $current = Utils::basename($this->cache_dir);
         $count = 0;
 
         foreach (new DirectoryIterator($cache_dir) as $file) {
@@ -618,11 +618,7 @@ class Cache extends Getters
      */
     public function isVolatileDriver($setting)
     {
-        if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
-            return true;
-        }
-
-        return false;
+        return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true);
     }
 
     /**

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/CompiledBase.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/CompiledBlueprints.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/CompiledConfig.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/CompiledLanguages.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/ConfigFileFinder.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Config/Languages.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 18 - 7
system/src/Grav/Common/Config/Setup.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Config
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -41,6 +41,9 @@ class Setup extends Data
      */
     public static $environment;
 
+    /** @var string */
+    public static $securityFile = 'config://security.yaml';
+
     /** @var array */
     protected $streams = [
         'user' => [
@@ -179,13 +182,14 @@ class Setup extends Data
         // If no environment is set, make sure we get one (CLI or hostname).
         if (null === $environment) {
             if (defined('GRAV_CLI')) {
+                $request = null;
+                $uri = null;
                 $environment = 'cli';
             } else {
                 /** @var ServerRequestInterface $request */
                 $request = $container['request'];
-                $host = $request->getUri()->getHost();
-
-                $environment = Utils::substrToString($host, ':');
+                $uri = $request->getUri();
+                $environment = $uri->getHost();
             }
         }
 
@@ -390,12 +394,19 @@ class Setup extends Data
 
             if (!$locator->findResource('environment://config', true)) {
                 // If environment does not have its own directory, remove it from the lookup.
-                $this->set('streams.schemes.environment.prefixes', ['config' => []]);
+                $prefixes = $this->get('streams.schemes.environment.prefixes');
+                $prefixes['config'] = [];
+
+                $this->set('streams.schemes.environment.prefixes', $prefixes);
                 $this->initializeLocator($locator);
             }
 
-            // Create security.yaml if it doesn't exist.
-            $filename = $locator->findResource('config://security.yaml', true, true);
+            // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
+            $securityFile = Utils::basename(static::$securityFile);
+            $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
+            $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
+            $filename = "{$securityFolder}/{$securityFile}";
+
             $security_file = CompiledYamlFile::instance($filename);
             $security_content = (array)$security_file->content();
 

+ 13 - 12
system/src/Grav/Common/Data/Blueprint.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -37,7 +37,7 @@ class Blueprint extends BlueprintForm
     /** @var string|null */
     protected $scope;
 
-    /** @var BlueprintSchema */
+    /** @var BlueprintSchema|null */
     protected $blueprintSchema;
 
     /** @var object|null */
@@ -54,7 +54,7 @@ class Blueprint extends BlueprintForm
      */
     public function __clone()
     {
-        if ($this->blueprintSchema) {
+        if (null !== $this->blueprintSchema) {
             $this->blueprintSchema = clone $this->blueprintSchema;
         }
     }
@@ -99,7 +99,7 @@ class Blueprint extends BlueprintForm
      */
     public function getDefaultValue(string $name)
     {
-        $path = explode('.', $name) ?: [];
+        $path = explode('.', $name);
         $current = $this->getDefaults();
 
         foreach ($path as $field) {
@@ -293,15 +293,16 @@ class Blueprint extends BlueprintForm
     /**
      * Flatten data by using blueprints.
      *
-     * @param  array $data
-     * @param  bool $includeAll
+     * @param array $data       Data to be flattened.
+     * @param bool $includeAll  True if undefined properties should also be included.
+     * @param string $name      Property which will be flattened, useful for flattening repeating data.
      * @return array
      */
-    public function flattenData(array $data, bool $includeAll = false)
+    public function flattenData(array $data, bool $includeAll = false, string $name = '')
     {
         $this->initInternals();
 
-        return $this->blueprintSchema->flattenData($data, $includeAll);
+        return $this->blueprintSchema->flattenData($data, $includeAll, $name);
     }
 
 
@@ -514,7 +515,7 @@ class Blueprint extends BlueprintForm
             $success = $this->resolveActions($user, $actions);
         }
         if (!$success) {
-            $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+            static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
         }
     }
 
@@ -565,7 +566,7 @@ class Blueprint extends BlueprintForm
         }
 
         if ($matches) {
-            $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+            static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
             return;
         }
     }
@@ -576,7 +577,7 @@ class Blueprint extends BlueprintForm
      * @param mixed $value
      * @return void
      */
-    protected function addPropertyRecursive(array &$field, $property, $value)
+    public static function addPropertyRecursive(array &$field, $property, $value)
     {
         if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
             $field[$property] = array_merge_recursive($field[$property], $value);
@@ -586,7 +587,7 @@ class Blueprint extends BlueprintForm
 
         if (!empty($field['fields'])) {
             foreach ($field['fields'] as $key => &$child) {
-                $this->addPropertyRecursive($child, $property, $value);
+                static::addPropertyRecursive($child, $property, $value);
             }
         }
     }

+ 26 - 10
system/src/Grav/Common/Data/BlueprintSchema.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -56,6 +56,15 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         return $this->types[$name] ?? [];
     }
 
+    /**
+     * @param string $name
+     * @return array|null
+     */
+    public function getNestedRules(string $name)
+    {
+        return $this->getNested($name);
+    }
+
     /**
      * Validate data against blueprints.
      *
@@ -74,7 +83,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         }
 
         if (!empty($messages)) {
-            throw (new ValidationException())->setMessages($messages);
+            throw (new ValidationException('', 400))->setMessages($messages);
         }
     }
 
@@ -106,23 +115,30 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     /**
      * Flatten data by using blueprints.
      *
-     * @param  array $data                  Data to be flattened.
-     * @param bool $includeAll
+     * @param array $data       Data to be flattened.
+     * @param bool $includeAll  True if undefined properties should also be included.
+     * @param string $name      Property which will be flattened, useful for flattening repeating data.
      * @return array
      */
-    public function flattenData(array $data, bool $includeAll = false)
+    public function flattenData(array $data, bool $includeAll = false, string $name = '')
     {
+        $prefix = $name !== '' ? $name . '.' : '';
+
         $list = [];
         if ($includeAll) {
-            foreach ($this->items as $key => $rules) {
+            $items = $name !== '' ? $this->getProperty($name)['fields'] ?? [] : $this->items;
+            foreach ($items as $key => $rules) {
                 $type = $rules['type'] ?? '';
-                if (!str_starts_with($type, '_') && !str_contains($key, '*')) {
-                    $list[$key] = null;
+                $ignore = (bool) array_filter((array)($rules['validate']['ignore'] ?? [])) ?? false;
+                if (!str_starts_with($type, '_') && !str_contains($key, '*') && $ignore !== true) {
+                    $list[$prefix . $key] = null;
                 }
             }
         }
 
-        return array_replace($list, $this->flattenArray($data, $this->nested, ''));
+        $nested = $this->getNestedRules($name);
+
+        return array_replace($list, $this->flattenArray($data, $nested, $prefix));
     }
 
     /**
@@ -190,7 +206,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                 /** @var Config $config */
                 $config = Grav::instance()['config'];
                 if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) {
-                    throw new RuntimeException(sprintf('%s is not defined in blueprints', $key));
+                    throw new RuntimeException(sprintf('%s is not defined in blueprints', $key), 400);
                 }
 
                 user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED);

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -264,7 +264,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
      */
     public function blueprints()
     {
-        if (!$this->blueprints) {
+        if (null === $this->blueprints) {
             $this->blueprints = new Blueprint();
         } elseif (is_callable($this->blueprints)) {
             // Lazy load blueprints.
@@ -335,6 +335,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
     /**
      * @return array
      */
+    #[\ReturnTypeWillChange]
     public function jsonSerialize()
     {
         return $this->items;

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 60 - 15
system/src/Grav/Common/Data/Validation.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -246,7 +246,9 @@ class Validation
             return false;
         }
 
-        $max = (int)($params['max'] ?? 0);
+        $multiline = isset($params['multiline']) && $params['multiline'];
+
+        $max = (int)($params['max'] ?? ($multiline ? 65536 : 2048));
         if ($max && $len > $max) {
             return false;
         }
@@ -256,7 +258,7 @@ class Validation
             return false;
         }
 
-        if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) {
+        if (!$multiline && preg_match('/\R/um', $value)) {
             return false;
         }
 
@@ -317,6 +319,10 @@ class Validation
      */
     public static function typeCommaList($value, array $params, array $field)
     {
+        if (!isset($params['max'])) {
+            $params['max'] = 2048;
+        }
+
         return is_array($value) ? true : self::typeText($value, $params, $field);
     }
 
@@ -379,6 +385,10 @@ class Validation
      */
     public static function typePassword($value, array $params, array $field)
     {
+        if (!isset($params['max'])) {
+            $params['max'] = 256;
+        }
+
         return self::typeText($value, $params, $field);
     }
 
@@ -519,17 +529,32 @@ class Validation
             return false;
         }
 
-        if (isset($params['min']) && $value < $params['min']) {
-            return false;
+        $value = (float)$value;
+
+        $min = 0;
+        if (isset($params['min'])) {
+            $min = (float)$params['min'];
+            if ($value < $min) {
+                return false;
+            }
         }
 
-        if (isset($params['max']) && $value > $params['max']) {
-            return false;
+        if (isset($params['max'])) {
+            $max = (float)$params['max'];
+            if ($value > $max) {
+                return false;
+            }
         }
 
-        $min = $params['min'] ?? 0;
+        if (isset($params['step'])) {
+            $step = (float)$params['step'];
+            // Count of how many steps we are above/below the minimum value.
+            $pos = ($value - $min) / $step;
 
-        return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
+            return is_int(static::filterNumber($pos, $params, $field));
+        }
+
+        return true;
     }
 
     /**
@@ -593,7 +618,7 @@ class Validation
      */
     public static function typeColor($value, array $params, array $field)
     {
-        return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
+        return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
     }
 
     /**
@@ -606,10 +631,18 @@ class Validation
      */
     public static function typeEmail($value, array $params, array $field)
     {
+        if (empty($value)) {
+            return false;
+        }
+
+        if (!isset($params['max'])) {
+            $params['max'] = 320;
+        }
+
         $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
 
         foreach ($values as $val) {
-            if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
+            if (!(self::typeText($val, $params, $field) && strpos($val, '@', 1))) {
                 return false;
             }
         }
@@ -627,6 +660,10 @@ class Validation
      */
     public static function typeUrl($value, array $params, array $field)
     {
+        if (!isset($params['max'])) {
+            $params['max'] = 2048;
+        }
+
         return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
     }
 
@@ -766,14 +803,22 @@ class Validation
         }
 
         // If creating new values is allowed, no further checks are needed.
-        if (!empty($field['selectize']['create'])) {
+        $validateOptions = $field['validate']['options'] ?? null;
+        if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') {
             return true;
         }
 
         $options = $field['options'] ?? [];
         $use = $field['use'] ?? 'values';
 
-        if (empty($field['selectize']) || empty($field['multiple'])) {
+        if ($validateOptions) {
+            // Use custom options structure.
+            foreach ($options as &$option) {
+                $option = $option[$validateOptions] ?? null;
+            }
+            unset($option);
+            $options = array_values($options);
+        } elseif (empty($field['selectize']) || empty($field['multiple'])) {
             $options = array_keys($options);
         }
         if ($use === 'keys') {
@@ -793,7 +838,7 @@ class Validation
     {
         $value = static::filterArray($value, $params, $field);
 
-        return Utils::arrayUnflattenDotNotation($value);
+        return is_array($value) ? Utils::arrayUnflattenDotNotation($value) : null;
     }
 
     /**
@@ -1174,7 +1219,7 @@ class Validation
      */
     public static function filterItem_List($value, $params)
     {
-        return array_values(array_filter($value, function ($v) {
+        return array_values(array_filter($value, static function ($v) {
             return !empty($v);
         }));
     }

+ 20 - 5
system/src/Grav/Common/Data/ValidationException.php

@@ -3,23 +3,25 @@
 /**
  * @package    Grav\Common\Data
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common\Data;
 
 use Grav\Common\Grav;
+use JsonSerializable;
 use RuntimeException;
 
 /**
  * Class ValidationException
  * @package Grav\Common\Data
  */
-class ValidationException extends RuntimeException
+class ValidationException extends RuntimeException implements JsonSerializable
 {
     /** @var array */
     protected $messages = [];
+    protected $escape = true;
 
     /**
      * @param array $messages
@@ -32,21 +34,34 @@ class ValidationException extends RuntimeException
         $language = Grav::instance()['language'];
         $this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
 
-        foreach ($messages as $variable => &$list) {
+        foreach ($messages as $list) {
             $list = array_unique($list);
             foreach ($list as $message) {
-                $this->message .= "<br/>$message";
+                $this->message .= '<br/>' . htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
             }
         }
 
         return $this;
     }
 
+    public function setSimpleMessage(bool $escape = true): void
+    {
+        $first = reset($this->messages);
+        $message = reset($first);
+
+        $this->message = $escape ? htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $message;
+    }
+
     /**
      * @return array
      */
-    public function getMessages()
+    public function getMessages(): array
     {
         return $this->messages;
     }
+
+    public function jsonSerialize(): array
+    {
+        return ['validation' => $this->messages];
+    }
 }

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -332,7 +332,7 @@ class Debugger
             return new Response(404, $headers, json_encode($response));
         }
 
-        $data = is_array($data) ? array_map(function ($item) {
+        $data = is_array($data) ? array_map(static function ($item) {
             return $item->toArray();
         }, $data) : $data->toArray();
 
@@ -856,6 +856,10 @@ class Debugger
             $scope = 'grav';
         } elseif (strpos($errfile, '/twig/') !== false) {
             $scope = 'twig';
+            // TODO: remove when upgrading to Twig 2+
+            if (str_contains($errstr, '#[\ReturnTypeWillChange]') || str_contains($errstr, 'Passing null to parameter')) {
+                return true;
+            }
         } elseif (stripos($errfile, '/yaml/') !== false) {
             $scope = 'yaml';
         } elseif (strpos($errfile, '/vendor/') !== false) {

+ 1 - 1
system/src/Grav/Common/Errors/BareHandler.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
system/src/Grav/Common/Errors/SimplePageHandler.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -57,7 +57,7 @@ class SimplePageHandler extends Handler
         $vars = array(
             'stylesheet' => file_get_contents($cssFile),
             'code'        => $code,
-            'message'     => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING),
+            'message'     => htmlspecialchars(strip_tags(rawurldecode($message)), ENT_QUOTES, 'UTF-8'),
         );
 
         $helper->setVariables($vars);

+ 22 - 1
system/src/Grav/Common/Errors/SystemFacade.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -43,4 +43,25 @@ class SystemFacade extends \Whoops\Util\SystemFacade
             $handler();
         }
     }
+
+
+    /**
+     * @param int $httpCode
+     *
+     * @return int
+     */
+    public function setHttpResponseCode($httpCode)
+    {
+        if (!headers_sent()) {
+            // Ensure that no 'location' header is present as otherwise this
+            // will override the HTTP code being set here, and mask the
+            // expected error page.
+            header_remove('location');
+
+            // Work around PHP bug #8218 (8.0.17 & 8.1.4).
+            header_remove('Content-Encoding');
+        }
+
+        return http_response_code($httpCode);
+    }
 }

+ 83 - 10
system/src/Grav/Common/File/CompiledFile.php

@@ -3,13 +3,16 @@
 /**
  * @package    Grav\Common\File
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common\File;
 
 use Exception;
+use Grav\Common\Debugger;
+use Grav\Common\Grav;
+use Grav\Common\Utils;
 use RocketTheme\Toolbox\File\PhpFile;
 use RuntimeException;
 use Throwable;
@@ -31,9 +34,10 @@ trait CompiledFile
     public function content($var = null)
     {
         try {
+            $filename = $this->filename;
             // If nothing has been loaded, attempt to get pre-compiled version of the file first.
             if ($var === null && $this->raw === null && $this->content === null) {
-                $key = md5($this->filename);
+                $key = md5($filename);
                 $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
 
                 $modified = $this->modified();
@@ -47,39 +51,49 @@ trait CompiledFile
 
                 $class = get_class($this);
 
+                $size = filesize($filename);
                 $cache = $file->exists() ? $file->content() : null;
 
                 // Load real file if cache isn't up to date (or is invalid).
                 if (!isset($cache['@class'])
                     || $cache['@class'] !== $class
                     || $cache['modified'] !== $modified
-                    || $cache['filename'] !== $this->filename
+                    || ($cache['size'] ?? null) !== $size
+                    || $cache['filename'] !== $filename
                 ) {
                     // Attempt to lock the file for writing.
                     try {
-                        $file->lock(false);
+                        $locked = $file->lock(false);
                     } catch (Exception $e) {
-                        // Another process has locked the file; we will check this in a bit.
+                        $locked = false;
+
+                        /** @var Debugger $debugger */
+                        $debugger = Grav::instance()['debugger'];
+                        $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
                     }
 
                     // Decode RAW file into compiled array.
                     $data = (array)$this->decode($this->raw());
                     $cache = [
                         '@class' => $class,
-                        'filename' => $this->filename,
+                        'filename' => $filename,
                         'modified' => $modified,
+                        'size' => $size,
                         'data' => $data
                     ];
 
                     // If compiled file wasn't already locked by another process, save it.
-                    if ($file->locked() !== false) {
+                    if ($locked) {
                         $file->save($cache);
                         $file->unlock();
 
                         // Compile cached file into bytecode cache
-                        if (function_exists('opcache_invalidate')) {
+                        if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
+                            $lockName = $file->filename();
+
                             // Silence error if function exists, but is restricted.
-                            @opcache_invalidate($file->filename(), true);
+                            @opcache_invalidate($lockName, true);
+                            @opcache_compile_file($lockName);
                         }
                     }
                 }
@@ -88,12 +102,71 @@ trait CompiledFile
                 $this->content = $cache['data'];
             }
         } catch (Exception $e) {
-            throw new RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
+            throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($filename), $e->getMessage()), 500, $e);
         }
 
         return parent::content($var);
     }
 
+    /**
+     * Save file.
+     *
+     * @param  mixed  $data  Optional data to be saved, usually array.
+     * @return void
+     * @throws RuntimeException
+     */
+    public function save($data = null)
+    {
+        // Make sure that the cache file is always up to date!
+        $key = md5($this->filename);
+        $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
+        try {
+            $locked = $file->lock();
+        } catch (Exception $e) {
+            $locked = false;
+
+            /** @var Debugger $debugger */
+            $debugger = Grav::instance()['debugger'];
+            $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
+        }
+
+        parent::save($data);
+
+        if ($locked) {
+            $modified = $this->modified();
+            $filename = $this->filename;
+            $class = get_class($this);
+            $size = filesize($filename);
+
+            // windows doesn't play nicely with this as it can't read when locked
+            if (!Utils::isWindows()) {
+                // Reload data from the filesystem. This ensures that we always cache the correct data (see issue #2282).
+                $this->raw = $this->content = null;
+                $data = (array)$this->decode($this->raw());
+            }
+
+            // Decode data into compiled array.
+            $cache = [
+                '@class' => $class,
+                'filename' => $filename,
+                'modified' => $modified,
+                'size' => $size,
+                'data' => $data
+            ];
+
+            $file->save($cache);
+            $file->unlock();
+
+            // Compile cached file into bytecode cache
+            if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
+                $lockName = $file->filename();
+                // Silence error if function exists, but is restricted.
+                @opcache_invalidate($lockName, true);
+                @opcache_compile_file($lockName);
+            }
+        }
+    }
+
     /**
      * Serialize file.
      *

+ 1 - 1
system/src/Grav/Common/File/CompiledJsonFile.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\File
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/File/CompiledMarkdownFile.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\File
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/File/CompiledYamlFile.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\File
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Filesystem/Archiver.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Filesystem
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 66 - 58
system/src/Grav/Common/Filesystem/Folder.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Filesystem
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -31,32 +31,34 @@ abstract class Folder
     /**
      * Recursively find the last modified time under given path.
      *
-     * @param  string $path
+     * @param array $paths
      * @return int
      */
-    public static function lastModifiedFolder($path)
+    public static function lastModifiedFolder(array $paths): int
     {
-        if (!file_exists($path)) {
-            return 0;
-        }
-
         $last_modified = 0;
 
         /** @var UniformResourceLocator $locator */
         $locator = Grav::instance()['locator'];
         $flags = RecursiveDirectoryIterator::SKIP_DOTS;
-        if ($locator->isStream($path)) {
-            $directory = $locator->getRecursiveIterator($path, $flags);
-        } else {
-            $directory = new RecursiveDirectoryIterator($path, $flags);
-        }
-        $filter  = new RecursiveFolderFilterIterator($directory);
-        $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
 
-        foreach ($iterator as $dir) {
-            $dir_modified = $dir->getMTime();
-            if ($dir_modified > $last_modified) {
-                $last_modified = $dir_modified;
+        foreach ($paths as $path) {
+            if (!file_exists($path)) {
+                return 0;
+            }
+            if ($locator->isStream($path)) {
+                $directory = $locator->getRecursiveIterator($path, $flags);
+            } else {
+                $directory = new RecursiveDirectoryIterator($path, $flags);
+            }
+            $filter  = new RecursiveFolderFilterIterator($directory);
+            $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
+
+            foreach ($iterator as $dir) {
+                $dir_modified = $dir->getMTime();
+                if ($dir_modified > $last_modified) {
+                    $last_modified = $dir_modified;
+                }
             }
         }
 
@@ -66,38 +68,40 @@ abstract class Folder
     /**
      * Recursively find the last modified time under given path by file.
      *
-     * @param string  $path
+     * @param array  $paths
      * @param string  $extensions   which files to search for specifically
      * @return int
      */
-    public static function lastModifiedFile($path, $extensions = 'md|yaml')
+    public static function lastModifiedFile(array $paths, $extensions = 'md|yaml'): int
     {
-        if (!file_exists($path)) {
-            return 0;
-        }
-
         $last_modified = 0;
 
         /** @var UniformResourceLocator $locator */
         $locator = Grav::instance()['locator'];
         $flags = RecursiveDirectoryIterator::SKIP_DOTS;
-        if ($locator->isStream($path)) {
-            $directory = $locator->getRecursiveIterator($path, $flags);
-        } else {
-            $directory = new RecursiveDirectoryIterator($path, $flags);
-        }
-        $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
-        $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
 
-        /** @var RecursiveDirectoryIterator $file */
-        foreach ($iterator as $filepath => $file) {
-            try {
-                $file_modified = $file->getMTime();
-                if ($file_modified > $last_modified) {
-                    $last_modified = $file_modified;
+        foreach($paths as $path) {
+            if (!file_exists($path)) {
+                return 0;
+            }
+            if ($locator->isStream($path)) {
+                $directory = $locator->getRecursiveIterator($path, $flags);
+            } else {
+                $directory = new RecursiveDirectoryIterator($path, $flags);
+            }
+            $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
+            $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
+
+            /** @var RecursiveDirectoryIterator $file */
+            foreach ($iterator as $file) {
+                try {
+                    $file_modified = $file->getMTime();
+                    if ($file_modified > $last_modified) {
+                        $last_modified = $file_modified;
+                    }
+                } catch (Exception $e) {
+                    Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
                 }
-            } catch (Exception $e) {
-                Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
             }
         }
 
@@ -107,28 +111,30 @@ abstract class Folder
     /**
      * Recursively md5 hash all files in a path
      *
-     * @param string $path
+     * @param array $paths
      * @return string
      */
-    public static function hashAllFiles($path)
+    public static function hashAllFiles(array $paths): string
     {
         $files = [];
 
-        if (file_exists($path)) {
-            $flags = RecursiveDirectoryIterator::SKIP_DOTS;
+        foreach ($paths as $path) {
+            if (file_exists($path)) {
+                $flags = RecursiveDirectoryIterator::SKIP_DOTS;
 
-            /** @var UniformResourceLocator $locator */
-            $locator = Grav::instance()['locator'];
-            if ($locator->isStream($path)) {
-                $directory = $locator->getRecursiveIterator($path, $flags);
-            } else {
-                $directory = new RecursiveDirectoryIterator($path, $flags);
-            }
+                /** @var UniformResourceLocator $locator */
+                $locator = Grav::instance()['locator'];
+                if ($locator->isStream($path)) {
+                    $directory = $locator->getRecursiveIterator($path, $flags);
+                } else {
+                    $directory = new RecursiveDirectoryIterator($path, $flags);
+                }
 
-            $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
+                $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
 
-            foreach ($iterator as $file) {
-                $files[] = $file->getPathname() . '?'. $file->getMTime();
+                foreach ($iterator as $file) {
+                    $files[] = $file->getPathname() . '?'. $file->getMTime();
+                }
             }
         }
 
@@ -197,7 +203,7 @@ abstract class Folder
      * Shift first directory out of the path.
      *
      * @param string $path
-     * @return string
+     * @return string|null
      */
     public static function shift(&$path)
     {
@@ -371,7 +377,7 @@ abstract class Folder
             return;
         }
 
-        if (strpos($target, $source) === 0) {
+        if (strpos($target, $source . '/') === 0) {
             throw new RuntimeException('Cannot move folder to itself');
         }
 
@@ -417,7 +423,8 @@ abstract class Folder
 
         if (!$success) {
             $error = error_get_last();
-            throw new RuntimeException($error['message']);
+
+            throw new RuntimeException($error['message'] ?? 'Unknown error');
         }
 
         // Make sure that the change will be detected when caching.
@@ -512,7 +519,7 @@ abstract class Folder
         }
         $directories = glob($directory . '/*', GLOB_ONLYDIR);
 
-        return count($directories);
+        return $directories ? count($directories) : false;
     }
 
     /**
@@ -529,7 +536,8 @@ abstract class Folder
         }
 
         // Go through all items in filesystem and recursively remove everything.
-        $files = array_diff(scandir($folder, SCANDIR_SORT_NONE), array('.', '..'));
+        $files = scandir($folder, SCANDIR_SORT_NONE);
+        $files = $files ? array_diff($files, ['.', '..']) : [];
         foreach ($files as $file) {
             $path = "{$folder}/{$file}";
             is_dir($path) ? self::doDelete($path) : @unlink($path);

+ 1 - 1
system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Filesystem
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Filesystem
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 4 - 5
system/src/Grav/Common/Filesystem/ZipArchiver.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Filesystem
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -57,7 +57,9 @@ class ZipArchiver extends Archiver
             throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
         }
 
-        if (!file_exists($source)) {
+        // Get real path for our folder
+        $rootPath = realpath($source);
+        if (!$rootPath) {
             throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
         }
 
@@ -66,9 +68,6 @@ class ZipArchiver extends Archiver
             throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
         }
 
-        // Get real path for our folder
-        $rootPath = realpath($source);
-
         $files = $this->getArchiveFiles($rootPath);
 
         $status && $status([

+ 1 - 1
system/src/Grav/Common/Flex/FlexCollection.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 4 - 3
system/src/Grav/Common/Flex/FlexObject.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
 
 use Grav\Common\Flex\Traits\FlexGravTrait;
 use Grav\Common\Flex\Traits\FlexObjectTrait;
+use Grav\Common\Media\Interfaces\MediaInterface;
 use Grav\Framework\Flex\Traits\FlexMediaTrait;
 use function is_array;
 
@@ -21,7 +22,7 @@ use function is_array;
  *
  * @package Grav\Common\Flex
  */
-abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
+abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
 {
     use FlexGravTrait;
     use FlexObjectTrait;
@@ -42,7 +43,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
 
         // Handle media fields.
         $settings = $this->getFieldSettings($name);
-        if ($settings['media_field'] ?? false === true) {
+        if (($settings['media_field'] ?? false) === true) {
             return $this->parseFileProperty($value, $settings);
         }
 

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

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Traits/FlexGravTrait.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Generic/GenericObject.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 46 - 18
system/src/Grav/Common/Flex/Types/Pages/PageCollection.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 /**
  * @package    Grav\Common\Flex
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -19,7 +19,6 @@ use Grav\Common\Page\Header;
 use Grav\Common\Page\Interfaces\PageCollectionInterface;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Utils;
-use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 use Grav\Framework\Flex\Pages\FlexPageCollection;
 use Collator;
 use InvalidArgumentException;
@@ -35,7 +34,9 @@ use function is_string;
  * Class GravPageCollection
  * @package Grav\Plugin\FlexObjects\Types\GravPages
  *
- * @extends FlexPageCollection<PageObject>
+ * @template T as PageObject
+ * @extends FlexPageCollection<T>
+ * @implements PageCollectionInterface<string,T>
  *
  * Incompatibilities with Grav\Common\Page\Collection:
  *     $page = $collection->key()       will not work at all
@@ -47,10 +48,6 @@ use function is_string;
  *     $collection->prev()              does not rewind the internal pointer
  * AND most methods are immutable; they do not update the current collection, but return updated one
  *
- * @method static shuffle()
- * @method static select(array $keys)
- * @method static unselect(array $keys)
- * @method static createFrom(array $elements, string $keyField = null)
  * @method PageIndex getIndex()
  */
 class PageCollection extends FlexPageCollection implements PageCollectionInterface
@@ -109,13 +106,11 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     }
 
     /**
-     * @return PageObject
+     * @return PageInterface
      */
     public function getRoot()
     {
-        $index = $this->getIndex();
-
-        return $index->getRoot();
+        return $this->getIndex()->getRoot();
     }
 
     /**
@@ -155,11 +150,11 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Add a single page to a collection
      *
      * @param PageInterface $page
-     * @return static
+     * @return $this
      */
     public function addPage(PageInterface $page)
     {
-        if (!$page instanceof FlexObjectInterface) {
+        if (!$page instanceof PageObject) {
             throw new InvalidArgumentException('$page is not a flex page.');
         }
 
@@ -175,6 +170,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param PageCollectionInterface $collection
      * @return static
+     * @phpstan-return static<T>
      */
     public function merge(PageCollectionInterface $collection)
     {
@@ -186,17 +182,26 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param PageCollectionInterface $collection
      * @return static
+     * @phpstan-return static<T>
      */
     public function intersect(PageCollectionInterface $collection)
     {
         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 PageInterface|false
-     * @phpstan-return PageObject|false
+     * @phpstan-return T|false
      */
     public function prev()
     {
@@ -211,7 +216,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Return nth item.
      * @param int $key
      * @return PageInterface|bool
-     * @phpstan-return PageObject|false
+     * @phpstan-return T|false
      */
     public function nth($key)
     {
@@ -223,6 +228,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param int $num Specifies how many entries should be picked.
      * @return static
+     * @phpstan-return static<T>
      */
     public function random($num = 1)
     {
@@ -234,6 +240,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param array $items Items to be appended. Existing keys will be overridden with the new values.
      * @return static
+     * @phpstan-return static<T>
      */
     public function append($items)
     {
@@ -245,6 +252,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param int $size
      * @return static[]
+     * @phpstan-return static<T>[]
      */
     public function batch($size): array
     {
@@ -266,6 +274,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * @param array|null  $manual
      * @param int|null $sort_flags
      * @return static
+     * @phpstan-return static<T>
      */
     public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
     {
@@ -326,7 +335,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
                     $list[$key] = $child->slug();
                     break;
                 case 'basename':
-                    $list[$key] = basename($key);
+                    $list[$key] = Utils::basename($key);
                     break;
                 case 'folder':
                     $list[$key] = $child->folder();
@@ -392,8 +401,8 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
             $i = count($manual);
             $new_list = [];
             foreach ($list as $key => $dummy) {
-                $child = $this[$key];
-                $order = array_search($child->slug, $manual, true);
+                $child = $this[$key] ?? null;
+                $order = $child ? array_search($child->slug, $manual, true) : false;
                 if ($order === false) {
                     $order = $i++;
                 }
@@ -434,6 +443,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * @param string|null $endDate
      * @param string|null $field
      * @return static
+     * @phpstan-return static<T>
      * @throws Exception
      */
     public function dateRange($startDate = null, $endDate = null, $field = null)
@@ -461,6 +471,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only visible pages
      *
      * @return static The collection with only visible pages
+     * @phpstan-return static<T>
      */
     public function visible()
     {
@@ -478,6 +489,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only non-visible pages
      *
      * @return static The collection with only non-visible pages
+     * @phpstan-return static<T>
      */
     public function nonVisible()
     {
@@ -495,6 +507,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only pages
      *
      * @return static The collection with only pages
+     * @phpstan-return static<T>
      */
     public function pages()
     {
@@ -516,6 +529,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only modules
      *
      * @return static The collection with only modules
+     * @phpstan-return static<T>
      */
     public function modules()
     {
@@ -537,6 +551,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Alias of modules()
      *
      * @return static
+     * @phpstan-return static<T>
      */
     public function modular()
     {
@@ -547,6 +562,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Alias of pages()
      *
      * @return static
+     * @phpstan-return static<T>
      */
     public function nonModular()
     {
@@ -557,6 +573,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only published pages
      *
      * @return static The collection with only published pages
+     * @phpstan-return static<T>
      */
     public function published()
     {
@@ -574,6 +591,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only non-published pages
      *
      * @return static The collection with only non-published pages
+     * @phpstan-return static<T>
      */
     public function nonPublished()
     {
@@ -591,6 +609,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only routable pages
      *
      * @return static The collection with only routable pages
+     * @phpstan-return static<T>
      */
     public function routable()
     {
@@ -608,6 +627,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Creates new collection with only non-routable pages
      *
      * @return static The collection with only non-routable pages
+     * @phpstan-return static<T>
      */
     public function nonRoutable()
     {
@@ -626,6 +646,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param string $type
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofType($type)
     {
@@ -644,6 +665,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param string[] $types
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofOneOfTheseTypes($types)
     {
@@ -662,6 +684,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param array $accessLevels
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofOneOfTheseAccessLevels($accessLevels)
     {
@@ -703,6 +726,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withOrdered(bool $bool = true)
     {
@@ -714,6 +738,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withModules(bool $bool = true)
     {
@@ -725,6 +750,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withPages(bool $bool = true)
     {
@@ -738,6 +764,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * @param string|null $languageCode
      * @param bool|null $fallback
      * @return static
+     * @phpstan-return static<T>
      */
     public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
     {
@@ -771,6 +798,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * @param array $filters
      * @param bool $recursive
      * @return static
+     * @phpstan-return static<T>
      */
     public function filterBy(array $filters, bool $recursive = false)
     {

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