ouidade 2 years ago
parent
commit
ef94f03cde
100 changed files with 1683 additions and 519 deletions
  1. 117 0
      CHANGELOG.md
  2. 0 1
      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. 19 11
      composer.json
  9. 221 220
      composer.lock
  10. 10 24
      index.php
  11. 120 39
      system/blueprints/config/system.yaml
  12. 1 1
      system/blueprints/flex/pages.yaml
  13. 2 0
      system/blueprints/flex/user-accounts.yaml
  14. 1 0
      system/blueprints/flex/user-groups.yaml
  15. 12 0
      system/blueprints/pages/default.yaml
  16. 9 9
      system/blueprints/pages/external.yaml
  17. 3 7
      system/blueprints/pages/partials/security.yaml
  18. 18 1
      system/blueprints/user/account.yaml
  19. 1 1
      system/config/media.yaml
  20. 5 0
      system/config/system.yaml
  21. 2 2
      system/defines.php
  22. 1 1
      system/install.php
  23. 6 6
      system/languages/es.yaml
  24. 3 3
      system/router.php
  25. 165 0
      system/src/DOMLettersIterator.php
  26. 158 0
      system/src/DOMWordsIterator.php
  27. 151 16
      system/src/Grav/Common/Assets.php
  28. 19 4
      system/src/Grav/Common/Assets/BaseAsset.php
  29. 207 0
      system/src/Grav/Common/Assets/BlockAssets.php
  30. 2 2
      system/src/Grav/Common/Assets/Css.php
  31. 2 2
      system/src/Grav/Common/Assets/InlineCss.php
  32. 2 2
      system/src/Grav/Common/Assets/InlineJs.php
  33. 46 0
      system/src/Grav/Common/Assets/InlineJsModule.php
  34. 2 2
      system/src/Grav/Common/Assets/Js.php
  35. 49 0
      system/src/Grav/Common/Assets/JsModule.php
  36. 43 0
      system/src/Grav/Common/Assets/Link.php
  37. 59 6
      system/src/Grav/Common/Assets/Pipeline.php
  38. 11 7
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  39. 1 1
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  40. 10 1
      system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
  41. 4 4
      system/src/Grav/Common/Backup/Backups.php
  42. 1 1
      system/src/Grav/Common/Browser.php
  43. 2 2
      system/src/Grav/Common/Cache.php
  44. 1 1
      system/src/Grav/Common/Composer.php
  45. 1 1
      system/src/Grav/Common/Config/CompiledBase.php
  46. 1 1
      system/src/Grav/Common/Config/CompiledBlueprints.php
  47. 1 1
      system/src/Grav/Common/Config/CompiledConfig.php
  48. 1 1
      system/src/Grav/Common/Config/CompiledLanguages.php
  49. 1 1
      system/src/Grav/Common/Config/Config.php
  50. 1 1
      system/src/Grav/Common/Config/ConfigFileFinder.php
  51. 1 1
      system/src/Grav/Common/Config/Languages.php
  52. 2 2
      system/src/Grav/Common/Config/Setup.php
  53. 7 6
      system/src/Grav/Common/Data/Blueprint.php
  54. 13 7
      system/src/Grav/Common/Data/BlueprintSchema.php
  55. 1 1
      system/src/Grav/Common/Data/Blueprints.php
  56. 2 1
      system/src/Grav/Common/Data/Data.php
  57. 1 1
      system/src/Grav/Common/Data/DataInterface.php
  58. 2 2
      system/src/Grav/Common/Data/Validation.php
  59. 1 1
      system/src/Grav/Common/Data/ValidationException.php
  60. 5 1
      system/src/Grav/Common/Debugger.php
  61. 1 1
      system/src/Grav/Common/Errors/BareHandler.php
  62. 1 1
      system/src/Grav/Common/Errors/Errors.php
  63. 1 1
      system/src/Grav/Common/Errors/SimplePageHandler.php
  64. 1 1
      system/src/Grav/Common/Errors/SystemFacade.php
  65. 3 2
      system/src/Grav/Common/File/CompiledFile.php
  66. 1 1
      system/src/Grav/Common/File/CompiledJsonFile.php
  67. 1 1
      system/src/Grav/Common/File/CompiledMarkdownFile.php
  68. 1 1
      system/src/Grav/Common/File/CompiledYamlFile.php
  69. 1 1
      system/src/Grav/Common/Filesystem/Archiver.php
  70. 4 3
      system/src/Grav/Common/Filesystem/Folder.php
  71. 1 1
      system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
  72. 1 1
      system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
  73. 1 1
      system/src/Grav/Common/Filesystem/ZipArchiver.php
  74. 1 1
      system/src/Grav/Common/Flex/FlexCollection.php
  75. 1 1
      system/src/Grav/Common/Flex/FlexIndex.php
  76. 1 1
      system/src/Grav/Common/Flex/FlexObject.php
  77. 1 1
      system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
  78. 1 1
      system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
  79. 1 1
      system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
  80. 1 1
      system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
  81. 1 1
      system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php
  82. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php
  83. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
  84. 1 1
      system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
  85. 35 14
      system/src/Grav/Common/Flex/Types/Pages/PageCollection.php
  86. 45 7
      system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
  87. 16 11
      system/src/Grav/Common/Flex/Types/Pages/PageObject.php
  88. 5 5
      system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
  89. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php
  90. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php
  91. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
  92. 1 1
      system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
  93. 2 2
      system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php
  94. 1 1
      system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php
  95. 2 2
      system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
  96. 1 1
      system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php
  97. 1 1
      system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php
  98. 2 1
      system/src/Grav/Common/Flex/Types/Users/Traits/UserObjectLegacyTrait.php
  99. 1 1
      system/src/Grav/Common/Flex/Types/Users/UserCollection.php
  100. 2 2
      system/src/Grav/Common/Flex/Types/Users/UserIndex.php

+ 117 - 0
CHANGELOG.md

@@ -1,3 +1,120 @@
+# 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
 

File diff suppressed because it is too large
+ 0 - 1
README.md


+ 1 - 1
assets/.gitkeep

@@ -1 +1 @@
-/* @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved. */
+/* @copyright  Copyright (c) 2015 - 2022 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 - 2022 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 - 2022 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 - 2022 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');
 

+ 19 - 11
composer.json

@@ -19,7 +19,7 @@
         "ext-zip": "*",
         "ext-dom": "*",
         "ext-libxml": "*",
-        "symfony/polyfill-mbstring": "~1.20",
+        "symfony/polyfill-mbstring": "~1.23",
         "symfony/polyfill-iconv": "^1.23",
         "symfony/polyfill-php74": "^1.23",
         "symfony/polyfill-php80": "^1.23",
@@ -27,10 +27,10 @@
         "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",
@@ -48,11 +48,10 @@
         "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",
@@ -60,11 +59,12 @@
         "itsgoingd/clockwork": "^5.0",
         "symfony/http-client": "^4.4",
         "composer/semver": "^1.4",
-        "rhukster/dom-sanitizer": "^1.0"
+        "rhukster/dom-sanitizer": "^1.0",
+        "multiavatar/multiavatar-php": "^1.0"
     },
     "require-dev": {
         "codeception/codeception": "^4.1",
-        "phpstan/phpstan": "^1.0",
+        "phpstan/phpstan": "^1.2",
         "phpstan/phpstan-deprecation-rules": "^1.0",
         "phpunit/php-code-coverage": "~9.2",
         "getgrav/markdowndocs": "^2.0",
@@ -83,7 +83,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,
@@ -97,9 +98,16 @@
             "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"
@@ -108,8 +116,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=520M system/src",
-        "phpstan-framework": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
+        "phpstan": "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"

File diff suppressed because it is too large
+ 221 - 220
composer.lock


+ 10 - 24
index.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav.Core
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -12,10 +12,6 @@ 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 || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
 
@@ -24,16 +20,6 @@ if (PHP_SAPI === 'cli-server') {
     }
 }
 
-// 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;
 }

+ 120 - 39
system/blueprints/config/system.yaml

@@ -888,9 +888,45 @@ form:
           title: PLUGIN_ADMIN.ASSETS
 
           fields:
-            assets_section:
+            general_config_section:
               type: section
-              title: PLUGIN_ADMIN.ASSETS
+              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.CSS_ASSETS
               underline: true
 
             assets.css_pipeline:
@@ -959,6 +995,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 +1044,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 +1060,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
@@ -1394,6 +1445,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
@@ -1723,35 +1786,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
@@ -1770,3 +1813,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
+
+
+

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

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

@@ -122,6 +122,8 @@ config:
       fields:
         - key
         - email
+        - username
+        - fullname
 
 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

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

+ 1 - 1
system/config/media.yaml

@@ -199,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

+ 5 - 0
system/config/system.yaml

@@ -127,6 +127,9 @@ 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
@@ -182,6 +185,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)
@@ -204,6 +208,7 @@ http:
 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:

+ 2 - 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 - 2022 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.25');
+define('GRAV_VERSION', '1.7.31');
 define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
 define('GRAV_TESTING', false);
 

+ 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 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

+ 3 - 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -22,9 +22,9 @@ if ($path !== '/index.php' && is_file($root . $path)) {
         // 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|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
+        || 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|yaml|yml|php|pl|py|cgi|twig|sh|bat)$`ui', $path)
+        || 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

+ 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();
+    }
+}

+ 151 - 16
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 - 2022 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 = [];
 
@@ -160,6 +174,10 @@ class Assets extends PropertyObject
      */
     public function add($asset)
     {
+        if (!$asset) {
+            return $this;
+        }
+
         $args = func_get_args();
 
         // More than one asset
@@ -184,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 !== '') {
@@ -193,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);
                 }
             }
         }
@@ -222,7 +243,7 @@ class Assets extends PropertyObject
             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;
         }
@@ -230,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';
@@ -269,6 +292,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.
      *
@@ -309,6 +342,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.
@@ -400,7 +452,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);
@@ -432,9 +484,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)
     {
-        return $this->render('css', $group, $attributes);
+        $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(self::LINK, $group, $attributes);
     }
 
     /**
@@ -444,8 +516,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;
     }
 }

+ 19 - 4
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 - 2022 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',
@@ -248,6 +249,7 @@ abstract class BaseAsset extends PropertyObject
      *
      * @return array
      */
+    #[\ReturnTypeWillChange]
     public function jsonSerialize()
     {
         return ['type' => $this->getType(), 'elements' => $this->getElements()];
@@ -265,4 +267,17 @@ abstract class BaseAsset extends PropertyObject
     {
         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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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";
+    }
+}

+ 59 - 6
system/src/Grav/Common/Assets/Pipeline.php

@@ -3,7 +3,7 @@
 /**
  * @package    Grav\Common\Assets
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -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 = '{\/\*# (.*?) \*\/}';
 
@@ -122,7 +126,7 @@ 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}";
 
@@ -169,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;
@@ -198,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')) {
@@ -223,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
@@ -262,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

+ 11 - 7
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -62,13 +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();
@@ -100,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);
         }
 

+ 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 - 2022 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 - 2022 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);

+ 4 - 4
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 - 2022 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,
             '/'
@@ -145,7 +145,7 @@ class Backups
     {
         $backups = static::getAvailableBackups();
 
-        return array_sum(array_column($backups, 'size'));
+        return $backups ? array_sum(array_column($backups, 'size')) : 0;
     }
 
     /**

+ 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -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) {

+ 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -401,7 +401,7 @@ class Setup extends Data
             }
 
             // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
-            $securityFile = basename(static::$securityFile);
+            $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}";

+ 7 - 6
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -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);
     }
 
 

+ 13 - 7
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -115,23 +115,29 @@ 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;
+                    $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));
     }
 
     /**

+ 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 1
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -816,7 +816,7 @@ class Validation
     {
         $value = static::filterArray($value, $params, $field);
 
-        return Utils::arrayUnflattenDotNotation($value);
+        return is_array($value) ? Utils::arrayUnflattenDotNotation($value) : null;
     }
 
     /**

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

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

+ 5 - 1
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 3 - 2
system/src/Grav/Common/File/CompiledFile.php

@@ -3,13 +3,14 @@
 /**
  * @package    Grav\Common\File
  *
- * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common\File;
 
 use Exception;
+use Grav\Common\Utils;
 use RocketTheme\Toolbox\File\PhpFile;
 use RuntimeException;
 use Throwable;
@@ -88,7 +89,7 @@ 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($this->filename), $e->getMessage()), 500, $e);
         }
 
         return parent::content($var);

+ 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 - 2022 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 - 2022 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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 4 - 3
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -513,7 +513,7 @@ abstract class Folder
         }
         $directories = glob($directory . '/*', GLOB_ONLYDIR);
 
-        return count($directories);
+        return $directories ? count($directories) : false;
     }
 
     /**
@@ -530,7 +530,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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 35 - 14
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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -34,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
@@ -46,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
@@ -108,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();
     }
 
     /**
@@ -154,7 +150,7 @@ 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)
     {
@@ -174,6 +170,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param PageCollectionInterface $collection
      * @return static
+     * @phpstan-return static<T>
      */
     public function merge(PageCollectionInterface $collection)
     {
@@ -185,6 +182,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param PageCollectionInterface $collection
      * @return static
+     * @phpstan-return static<T>
      */
     public function intersect(PageCollectionInterface $collection)
     {
@@ -203,7 +201,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Return previous item.
      *
      * @return PageInterface|false
-     * @phpstan-return PageObject|false
+     * @phpstan-return T|false
      */
     public function prev()
     {
@@ -218,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)
     {
@@ -230,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)
     {
@@ -241,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)
     {
@@ -252,6 +252,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param int $size
      * @return static[]
+     * @phpstan-return static<T>[]
      */
     public function batch($size): array
     {
@@ -273,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)
     {
@@ -333,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();
@@ -441,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)
@@ -468,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()
     {
@@ -485,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()
     {
@@ -502,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()
     {
@@ -523,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()
     {
@@ -544,6 +551,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Alias of modules()
      *
      * @return static
+     * @phpstan-return static<T>
      */
     public function modular()
     {
@@ -554,6 +562,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      * Alias of pages()
      *
      * @return static
+     * @phpstan-return static<T>
      */
     public function nonModular()
     {
@@ -564,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()
     {
@@ -581,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()
     {
@@ -598,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()
     {
@@ -615,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()
     {
@@ -633,6 +646,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param string $type
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofType($type)
     {
@@ -651,6 +665,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param string[] $types
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofOneOfTheseTypes($types)
     {
@@ -669,6 +684,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
      *
      * @param array $accessLevels
      * @return static The collection
+     * @phpstan-return static<T>
      */
     public function ofOneOfTheseAccessLevels($accessLevels)
     {
@@ -710,6 +726,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withOrdered(bool $bool = true)
     {
@@ -721,6 +738,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withModules(bool $bool = true)
     {
@@ -732,6 +750,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
     /**
      * @param bool $bool
      * @return static
+     * @phpstan-return static<T>
      */
     public function withPages(bool $bool = true)
     {
@@ -745,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)
     {
@@ -778,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)
     {

+ 45 - 7
system/src/Grav/Common/Flex/Types/Pages/PageIndex.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -39,7 +39,10 @@ use function is_string;
  * Class GravPageObject
  * @package Grav\Plugin\FlexObjects\Types\GravPages
  *
- * @extends FlexPageIndex<PageObject,PageCollection>
+ * @template T of PageObject
+ * @template C of PageCollection
+ * @extends FlexPageIndex<T,C>
+ * @implements PageCollectionInterface<string,T>
  *
  * @method PageIndex withModules(bool $bool = true)
  * @method PageIndex withPages(bool $bool = true)
@@ -101,6 +104,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
     /**
      * @param string $key
      * @return PageObject|null
+     * @phpstan-return T|null
      */
     public function get($key)
     {
@@ -117,11 +121,13 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
             $element = $element->getTranslation(ltrim($params, '.'));
         }
 
+        \assert(null === $element || $element instanceof PageObject);
+
         return $element;
     }
 
     /**
-     * @return PageObject
+     * @return PageInterface
      */
     public function getRoot()
     {
@@ -172,7 +178,8 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
     /**
      * @param string|null $languageCode
      * @param bool|null $fallback
-     * @return PageIndex
+     * @return static
+     * @phpstan-return static<T,C>
      */
     public function withTranslated(string $languageCode = null, bool $fallback = null)
     {
@@ -276,6 +283,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * @param array $filters
      * @param bool $recursive
      * @return static
+     * @phpstan-return static<T,C>
      */
     public function filterBy(array $filters, bool $recursive = false)
     {
@@ -332,6 +340,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
     /**
      * @param array $filters
      * @return static
+     * @phpstan-return static<T,C>
      */
     protected function filterByParent(array $filters)
     {
@@ -402,6 +411,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * @param array $entries
      * @param string|null $keyField
      * @return static
+     * @phpstan-return static<T,C>
      */
     protected function createFrom(array $entries, string $keyField = null)
     {
@@ -596,8 +606,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                 }
             }
 
+            /** @var PageCollection|PageIndex $children */
+            $children = $page->children();
             /** @var PageIndex $children */
-            $children = $page->children()->getIndex();
+            $children = $children->getIndex();
             $selectedChildren = $children->filterBy($filters, true);
 
             /** @var Header $header */
@@ -627,7 +639,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                     $payload = [
                         'name' => $child->menu(),
                         'value' => $child->rawRoute(),
-                        'item-key' => basename($child->rawRoute() ?? ''),
+                        'item-key' => Utils::basename($child->rawRoute() ?? ''),
                         'filename' => $child->folder(),
                         'extension' => $child->extension(),
                         'type' => 'dir',
@@ -676,13 +688,15 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                     $extras = array_filter($extras, static function ($v) {
                         return $v !== null;
                     });
+
+                    /** @var PageIndex $tmp */
                     $tmp = $child->children()->getIndex();
                     $child_count = $tmp->count();
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
                     $route = $child->getRoute();
                     $route = $route ? ($route->toString(false) ?: '/') : '';
                     $payload = [
-                        'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
+                        'item-key' => htmlspecialchars(Utils::basename($child->rawRoute() ?? $child->getKey())),
                         'icon' => $icon,
                         'title' => htmlspecialchars($child->menu()),
                         'route' => [
@@ -787,6 +801,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param PageInterface $page
      * @return PageCollection
+     * @phpstan-return C
      */
     public function addPage(PageInterface $page)
     {
@@ -798,6 +813,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Create a copy of this collection
      *
      * @return static
+     * @phpstan-return static<T,C>
      */
     public function copy()
     {
@@ -810,6 +826,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param PageCollectionInterface $collection
      * @return PageCollection
+     * @phpstan-return C
      */
     public function merge(PageCollectionInterface $collection)
     {
@@ -822,6 +839,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param PageCollectionInterface $collection
      * @return PageCollection
+     * @phpstan-return C
      */
     public function intersect(PageCollectionInterface $collection)
     {
@@ -833,6 +851,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param int $size
      * @return PageCollection[]
+     * @phpstan-return C[]
      */
     public function batch($size)
     {
@@ -844,6 +863,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param string $key
      * @return PageObject|null
+     * @phpstan-return T|null
      * @throws InvalidArgumentException
      */
     public function remove($key)
@@ -859,6 +879,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * @param array  $manual
      * @param string $sort_flags
      * @return static
+     * @phpstan-return static<T,C>
      */
     public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
     {
@@ -902,6 +923,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param  string $path
      * @return PageObject|null  The previous item.
+     * @phpstan-return T|null
      */
     public function prevSibling($path)
     {
@@ -916,6 +938,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param  string $path
      * @return PageObject|null The next item.
+     * @phpstan-return T|null
      */
     public function nextSibling($path)
     {
@@ -931,6 +954,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * @param  string  $path
      * @param  int $direction either -1 or +1
      * @return PageObject|false    The sibling item.
+     * @phpstan-return T|false
      */
     public function adjacentSibling($path, $direction = 1)
     {
@@ -964,6 +988,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * @param string|null $endDate
      * @param string|null $field
      * @return static
+     * @phpstan-return static<T,C>
      * @throws Exception
      */
     public function dateRange($startDate = null, $endDate = null, $field = null)
@@ -988,6 +1013,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only visible pages
      *
      * @return static The collection with only visible pages
+     * @phpstan-return static<T,C>
      */
     public function visible()
     {
@@ -1000,6 +1026,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only non-visible pages
      *
      * @return static The collection with only non-visible pages
+     * @phpstan-return static<T,C>
      */
     public function nonVisible()
     {
@@ -1012,6 +1039,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only non-modular pages
      *
      * @return static The collection with only non-modular pages
+     * @phpstan-return static<T,C>
      */
     public function pages()
     {
@@ -1024,6 +1052,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only modular pages
      *
      * @return static The collection with only modular pages
+     * @phpstan-return static<T,C>
      */
     public function modules()
     {
@@ -1036,6 +1065,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only modular pages
      *
      * @return static The collection with only modular pages
+     * @phpstan-return static<T,C>
      */
     public function modular()
     {
@@ -1046,6 +1076,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only non-modular pages
      *
      * @return static The collection with only non-modular pages
+     * @phpstan-return static<T,C>
      */
     public function nonModular()
     {
@@ -1056,6 +1087,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only published pages
      *
      * @return static The collection with only published pages
+     * @phpstan-return static<T,C>
      */
     public function published()
     {
@@ -1068,6 +1100,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only non-published pages
      *
      * @return static The collection with only non-published pages
+     * @phpstan-return static<T,C>
      */
     public function nonPublished()
     {
@@ -1080,6 +1113,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only routable pages
      *
      * @return static The collection with only routable pages
+     * @phpstan-return static<T,C>
      */
     public function routable()
     {
@@ -1092,6 +1126,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      * Creates new collection with only non-routable pages
      *
      * @return static The collection with only non-routable pages
+     * @phpstan-return static<T,C>
      */
     public function nonRoutable()
     {
@@ -1105,6 +1140,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param string $type
      * @return static The collection
+     * @phpstan-return static<T,C>
      */
     public function ofType($type)
     {
@@ -1118,6 +1154,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param string[] $types
      * @return static The collection
+     * @phpstan-return static<T,C>
      */
     public function ofOneOfTheseTypes($types)
     {
@@ -1131,6 +1168,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
      *
      * @param array $accessLevels
      * @return static The collection
+     * @phpstan-return static<T,C>
      */
     public function ofOneOfTheseAccessLevels($accessLevels)
     {

+ 16 - 11
system/src/Grav/Common/Flex/Types/Pages/PageObject.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -25,9 +25,9 @@ use Grav\Common\Page\Pages;
 use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
-use Grav\Framework\Flex\FlexObject;
 use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 use Grav\Framework\Flex\Pages\FlexPageObject;
+use Grav\Framework\Object\ObjectCollection;
 use Grav\Framework\Route\Route;
 use Grav\Framework\Route\RouteFactory;
 use Grav\Plugin\Admin\Admin;
@@ -93,11 +93,6 @@ class PageObject extends FlexPageObject
         }
     }
 
-    public function translated(): bool
-    {
-        return $this->translatedLanguages(true) ? true : false;
-    }
-
     /**
      * @param string|array $query
      * @return Route|null
@@ -282,7 +277,7 @@ class PageObject extends FlexPageObject
 
     /**
      * @param array|bool $reorder
-     * @return FlexObject|FlexObjectInterface
+     * @return static
      */
     public function save($reorder = true)
     {
@@ -317,7 +312,7 @@ class PageObject extends FlexPageObject
     }
 
     /**
-     * @return PageObject
+     * @return static
      */
     public function delete()
     {
@@ -391,6 +386,7 @@ class PageObject extends FlexPageObject
     /**
      * @param array $ordering
      * @return PageCollection|null
+     * @phpstan-return ObjectCollection<string,PageObject>|null
      */
     protected function reorderSiblings(array $ordering)
     {
@@ -466,7 +462,9 @@ class PageObject extends FlexPageObject
         if ($isMoved && $this->order() !== false) {
             $parentKey = $this->getProperty('parent_key');
             if ($parentKey === '') {
-                $newParent = $this->getFlexDirectory()->getIndex()->getRoot();
+                /** @var PageIndex $index */
+                $index = $this->getFlexDirectory()->getIndex();
+                $newParent = $index->getRoot();
             } else {
                 $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
                 if (!$newParent instanceof PageInterface) {
@@ -627,7 +625,14 @@ class PageObject extends FlexPageObject
 
             // If current filter does not match, we still may have match as a parent.
             if ($matches === false) {
-                return $recursive && $this->children()->getIndex()->filterBy($filters, true)->count() > 0;
+                if (!$recursive) {
+                    return false;
+                }
+
+                /** @var PageIndex $index */
+                $index = $this->children()->getIndex();
+
+                return $index->filterBy($filters, true)->count() > 0;
             }
         }
 

+ 5 - 5
system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -16,13 +16,13 @@ use Grav\Common\Debugger;
 use Grav\Common\Flex\Types\Pages\PageIndex;
 use Grav\Common\Grav;
 use Grav\Common\Language\Language;
+use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\Storage\FolderStorage;
 use RocketTheme\Toolbox\File\MarkdownFile;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RuntimeException;
 use SplFileInfo;
-use function assert;
 use function in_array;
 use function is_string;
 
@@ -277,7 +277,7 @@ class PageStorage extends FolderStorage
         } else {
             $params = $template = $language = '';
         }
-        $objectKey = basename($key);
+        $objectKey = Utils::basename($key);
         if (preg_match('|^(\d+)\.(.+)$|', $objectKey, $matches)) {
             [, $order, $folder] = $matches;
         } else {
@@ -584,7 +584,7 @@ class PageStorage extends FolderStorage
                             $mark = $matches[2] ?? '';
                             $ext = $matches[1] ?? '';
                             $ext .= $this->dataExt;
-                            $markdown[$mark][basename($k, $ext)] = $timestamp;
+                            $markdown[$mark][Utils::basename($k, $ext)] = $timestamp;
                         }
 
                         $modified = max($modified, $timestamp);
@@ -675,7 +675,7 @@ class PageStorage extends FolderStorage
 
                 /** @phpstan-var array{'storage_key': string, 'storage_timestamp': int, 'children': array<string, mixed>} $parent */
                 $parent = &$list[$parentKey];
-                $basename = basename($storage_key);
+                $basename = Utils::basename($storage_key);
 
                 if (isset($parent['children'][$basename])) {
                     $timestamp = $meta['storage_timestamp'];

+ 1 - 1
system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -27,7 +27,7 @@ class UserGroupCollection extends FlexCollection
     public static function getCachedMethods(): array
     {
         return [
-            'authorize' => 'session',
+            'authorize' => false,
         ] + parent::getCachedMethods();
     }
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -37,7 +37,7 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
     public static function getCachedMethods(): array
     {
         return [
-            'authorize' => 'session',
+            'authorize' => false,
         ] + parent::getCachedMethods();
     }
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 1 - 1
system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 1
system/src/Grav/Common/Flex/Types/Users/Traits/UserObjectLegacyTrait.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -84,6 +84,7 @@ trait UserObjectLegacyTrait
      * @return int
      * @deprecated 1.6 Method makes no sense for user account.
      */
+    #[\ReturnTypeWillChange]
     public function count()
     {
         user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);

+ 1 - 1
system/src/Grav/Common/Flex/Types/Users/UserCollection.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 2 - 2
system/src/Grav/Common/Flex/Types/Users/UserIndex.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 - 2022 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -164,7 +164,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
      */
     protected static function filterUsername(string $key, FlexStorageInterface $storage): string
     {
-        return $storage->normalizeKey($key);
+        return method_exists($storage, 'normalizeKey') ? $storage->normalizeKey($key) : $key;
     }
 
     /**

Some files were not shown because too many files changed in this diff