Browse Source

update grav et plugin

Kevin 3 years ago
parent
commit
72aae1eb2d
100 changed files with 3924 additions and 840 deletions
  1. 127 3
      CHANGELOG.md
  2. 8 1
      bin/gpm
  3. 11 4
      bin/grav
  4. 8 6
      bin/plugin
  5. 18 13
      composer.json
  6. 181 252
      composer.lock
  7. 1 1
      fixperms.sh
  8. 10 9
      index.php
  9. 5 0
      system/aliases.php
  10. 0 1
      system/assets/jquery/jquery-3.x.min.js
  11. 64 7
      system/blueprints/config/system.yaml
  12. 11 0
      system/config/system.yaml
  13. 1 1
      system/defines.php
  14. 5 1
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  15. 2 0
      system/src/Grav/Common/Browser.php
  16. 4 4
      system/src/Grav/Common/Data/BlueprintSchema.php
  17. 30 2
      system/src/Grav/Common/Data/Data.php
  18. 10 1
      system/src/Grav/Common/Data/Validation.php
  19. 1 1
      system/src/Grav/Common/Debugger.php
  20. 26 12
      system/src/Grav/Common/Filesystem/Folder.php
  21. 2 1
      system/src/Grav/Common/GPM/Local/Package.php
  22. 1 1
      system/src/Grav/Common/GPM/Response.php
  23. 47 4
      system/src/Grav/Common/Grav.php
  24. 2 0
      system/src/Grav/Common/Inflector.php
  25. 2 1
      system/src/Grav/Common/Markdown/Parsedown.php
  26. 2 1
      system/src/Grav/Common/Markdown/ParsedownExtra.php
  27. 1 1
      system/src/Grav/Common/Media/Traits/MediaTrait.php
  28. 2 1
      system/src/Grav/Common/Page/Markdown/Excerpts.php
  29. 9 4
      system/src/Grav/Common/Page/Medium/ImageMedium.php
  30. 14 10
      system/src/Grav/Common/Page/Page.php
  31. 42 22
      system/src/Grav/Common/Page/Pages.php
  32. 1 1
      system/src/Grav/Common/Plugin.php
  33. 48 0
      system/src/Grav/Common/Processors/InitializeProcessor.php
  34. 2 2
      system/src/Grav/Common/Scheduler/Cron.php
  35. 13 0
      system/src/Grav/Common/Service/PagesServiceProvider.php
  36. 1 1
      system/src/Grav/Common/Twig/TwigExtension.php
  37. 22 17
      system/src/Grav/Common/Uri.php
  38. 140 1
      system/src/Grav/Console/ConsoleCommand.php
  39. 1 1
      system/src/Grav/Console/Gpm/SelfupgradeCommand.php
  40. 20 2
      system/src/Grav/Framework/File/Formatter/CsvFormatter.php
  41. 1 1
      system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
  42. 1554 0
      system/src/Grav/Framework/Parsedown/Parsedown.php
  43. 532 0
      system/src/Grav/Framework/Parsedown/ParsedownExtra.php
  44. 3 4
      system/src/Grav/Framework/Route/Route.php
  45. 30 24
      system/src/Grav/Framework/Session/Session.php
  46. 1 0
      user/plugins/admin/.gitignore
  47. 65 15
      user/plugins/admin/CHANGELOG.md
  48. 1 0
      user/plugins/admin/admin.yaml
  49. 16 2
      user/plugins/admin/blueprints.yaml
  50. 4 1
      user/plugins/admin/classes/admin.php
  51. 3 3
      user/plugins/admin/classes/adminbasecontroller.php
  52. 15 1
      user/plugins/admin/classes/admincontroller.php
  53. 242 180
      user/plugins/admin/composer.lock
  54. 6 0
      user/plugins/admin/languages/en.yaml
  55. 2 0
      user/plugins/admin/languages/pt.yaml
  56. 0 0
      user/plugins/admin/themes/grav/css-compiled/preset.css.map
  57. 0 0
      user/plugins/admin/themes/grav/css-compiled/template.css
  58. 0 0
      user/plugins/admin/themes/grav/css-compiled/template.css.map
  59. 1 1
      user/plugins/admin/themes/grav/scss/fonts.scss
  60. 2 1
      user/plugins/admin/themes/grav/scss/template/_admin.scss
  61. 1 0
      user/plugins/admin/themes/grav/scss/template/_login.scss
  62. 4 1
      user/plugins/admin/themes/grav/scss/template/modules/_buttons.scss
  63. 2 2
      user/plugins/admin/themes/grav/templates/forms/field.html.twig
  64. 4 1
      user/plugins/admin/themes/grav/templates/forms/fields/editor/editor.html.twig
  65. 6 3
      user/plugins/admin/themes/grav/templates/forms/fields/fieldset/fieldset.html.twig
  66. 24 1
      user/plugins/admin/themes/grav/templates/pages.html.twig
  67. 4 0
      user/plugins/admin/themes/grav/templates/partials/base-root.html.twig
  68. 1 1
      user/plugins/admin/themes/grav/templates/partials/nav-quick-tray.html.twig
  69. 1 1
      user/plugins/admin/themes/grav/templates/partials/nav-user-avatar.html.twig
  70. 42 0
      user/plugins/admin/themes/grav/templates/partials/noscript.html.twig
  71. 1 1
      user/plugins/admin/themes/grav/templates/partials/plugins-list.html.twig
  72. 2 2
      user/plugins/admin/vendor/composer/ClassLoader.php
  73. 9 9
      user/plugins/admin/vendor/composer/installed.json
  74. 12 0
      user/plugins/admin/vendor/composer/semver/CHANGELOG.md
  75. 1 2
      user/plugins/admin/vendor/composer/semver/composer.json
  76. 1 1
      user/plugins/admin/vendor/composer/semver/src/Constraint/AbstractConstraint.php
  77. 3 7
      user/plugins/admin/vendor/composer/semver/src/Constraint/Constraint.php
  78. 1 1
      user/plugins/admin/vendor/composer/semver/src/Constraint/EmptyConstraint.php
  79. 1 1
      user/plugins/admin/vendor/composer/semver/src/Constraint/MultiConstraint.php
  80. 2 2
      user/plugins/admin/vendor/composer/semver/src/Semver.php
  81. 22 25
      user/plugins/admin/vendor/composer/semver/src/VersionParser.php
  82. 41 0
      user/plugins/email/CHANGELOG.md
  83. 19 0
      user/plugins/email/README.md
  84. 6 3
      user/plugins/email/blueprints.yaml
  85. 3 2
      user/plugins/email/classes/Email.php
  86. 5 0
      user/plugins/email/cli/ClearQueueFailuresCommand.php
  87. 5 0
      user/plugins/email/cli/FlushQueueCommand.php
  88. 5 4
      user/plugins/email/cli/TestEmailCommand.php
  89. 126 60
      user/plugins/email/composer.lock
  90. 24 0
      user/plugins/email/email.php
  91. 2 2
      user/plugins/email/vendor/composer/ClassLoader.php
  92. 1 1
      user/plugins/email/vendor/composer/autoload_files.php
  93. 0 1
      user/plugins/email/vendor/composer/autoload_namespaces.php
  94. 1 0
      user/plugins/email/vendor/composer/autoload_psr4.php
  95. 3 0
      user/plugins/email/vendor/composer/autoload_real.php
  96. 7 10
      user/plugins/email/vendor/composer/autoload_static.php
  97. 131 66
      user/plugins/email/vendor/composer/installed.json
  98. 1 1
      user/plugins/email/vendor/doctrine/lexer/LICENSE
  99. 4 0
      user/plugins/email/vendor/doctrine/lexer/README.md
  100. 23 6
      user/plugins/email/vendor/doctrine/lexer/composer.json

+ 127 - 3
CHANGELOG.md

@@ -1,3 +1,127 @@
+# v1.6.27
+## 09/01/2020
+
+1. [](#improved)
+    * Right trim route for safety
+    * Use the proper ellipsis for summary [#2939](https://github.com/getgrav/grav/pull/2939)
+    * Left pad schedule times with zeros [#2921](https://github.com/getgrav/grav/pull/2921)
+
+# v1.6.26
+## 06/08/2020
+
+1. [](#improved)
+    * Added new configuration option to control the supported attributes in markdown links [#2882](https://github.com/getgrav/grav/issues/2882)
+1. [](#bugfix)
+    * Fixed blueprint for `system.pages.hide_empty_folders` [#1925](https://github.com/getgrav/grav/issues/2925)
+    * JSON Route of homepage with no ‘route’ set is valid
+    * Fix case-insensitive search of location header [form#425](https://github.com/getgrav/grav-plugin-form/issues/425)
+
+# v1.6.25
+## 05/14/2020
+
+1. [](#improved)
+    * Added system configuration support for `HTTP_X_Forwarded` headers (host disabled by default)
+    * Updated `PHPUserAgentParser` to 1.0.0
+    * Bump `Go` to version 1.13 in `travis.yaml`
+
+# v1.6.24
+## 04/27/2020
+
+1. [](#improved)
+    * Added support for `X-Forwarded-Host` [#2891](https://github.com/getgrav/grav/pull/2891)
+    * Disable XDebug in Travis builds
+
+# v1.6.23
+## 03/19/2020
+
+1. [](#new)
+    * Moved `Parsedown` 1.6 and `ParsedownExtra` 0.7 into `Grav\Framework\Parsedown` to allow fixes
+    * Added `aliases.php` with references to direct `\Parsedown` and `\ParsedownExtra` references
+1. [](#improved)
+    * Upgraded `jQuery` to latest 3.4.1 version [#2859](https://github.com/getgrav/grav/issues/2859)
+1. [](#bugfix)
+    * Fixed PHP 7.4 issue in ParsedownExtra [#2832](https://github.com/getgrav/grav/issues/2832)
+    * Fix for [user reported](https://twitter.com/OriginalSicksec) CVE path-based open redirect
+    * Fix for `stream_set_option` error with PHP 7.4 via Toolbox#28 [#2850](https://github.com/getgrav/grav/issues/2850)
+
+# v1.6.22
+## 03/05/2020
+
+1. [](#new)
+    * Added `Pages::reset()` method
+1. [](#improved)
+    * Updated Negotiation library to address issues [#2513](https://github.com/getgrav/grav/issues/2513)
+1. [](#bugfix)
+    * Fixed issue with search plugins not being able to switch between page translations
+    * Fixed issues with `Pages::baseRoute()` not picking up active language reliably
+    * Reverted `validation: strict` fix as it breaks sites, see [#1273](https://github.com/getgrav/grav/issues/1273)
+
+# v1.6.21
+## 02/11/2020
+
+1. [](#new)
+    * Added `ConsoleCommand::setLanguage()` method to set language to be used from CLI
+    * Added `ConsoleCommand::initializeGrav()` method to properly set up Grav instance to be used from CLI
+    * Added `ConsoleCommand::initializePlugins()`method to properly set up all plugins to be used from CLI
+    * Added `ConsoleCommand::initializeThemes()`method to properly set up current theme to be used from CLI
+    * Added `ConsoleCommand::initializePages()` method to properly set up pages to be used from CLI
+1. [](#improved)
+    * Vendor updates
+1. [](#bugfix)
+    * Fixed `bin/plugin` CLI calling `$themes->init()` way too early (removed it, use above methods instead)
+    * Fixed call to `$grav['page']` crashing CLI
+    * Fixed encoding problems when PHP INI setting `default_charset` is not `utf-8` [#2154](https://github.com/getgrav/grav/issues/2154)
+
+# v1.6.20
+## 02/03/2020
+
+1. [](#bugfix)
+    * Fixed incorrect routing caused by `str_replace()` in `Uri::init()` [#2754](https://github.com/getgrav/grav/issues/2754)
+    * Fixed session cookie is being set twice in the HTTP header [#2745](https://github.com/getgrav/grav/issues/2745)
+    * Fixed session not restarting if user was invalid (downgrading from Grav 1.7)
+    * Fixed filesystem iterator calls with non-existing folders
+    * Fixed `checkbox` field not being saved, requires also Form v4.0.2 [#1225](https://github.com/getgrav/grav/issues/1225)
+    * Fixed `validation: strict` not working in blueprints [#1273](https://github.com/getgrav/grav/issues/1273)
+    * Fixed `Data::filter()` removing empty fields (such as empty list) by default [#2805](https://github.com/getgrav/grav/issues/2805)
+    * Fixed fatal error with non-integer page param value [#2803](https://github.com/getgrav/grav/issues/2803)
+    * Fixed `Assets::addInlineJs()` parameter type mismatch between v1.5 and v1.6 [#2659](https://github.com/getgrav/grav/issues/2659)
+    * Fixed `site.metadata` saving issues [#2615](https://github.com/getgrav/grav/issues/2615)
+
+# v1.6.19
+## 12/04/2019
+
+1. [](#new)
+    * Catch PHP 7.4 deprecation messages and report them in debugbar instead of throwing fatal error
+1. [](#bugfix)
+    * Fixed fatal error when calling `{{ grav.undefined }}`
+    * Fixed multiple issues when there are no pages in the site
+    * PHP 7.4 fix for [#2750](https://github.com/getgrav/grav/issues/2750)
+
+# v1.6.18
+## 12/02/2019
+
+1. [](#bugfix)
+    * PHP 7.4 fix in `Pages::buildSort()`
+    * Updated vendor libraries for PHP 7.4 fixes in Twig and other libraries
+    * Fixed fatal error when `$page->id()` is null [#2731](https://github.com/getgrav/grav/pull/2731)
+    * Fixed cache conflicts on pages with no set id
+    * Fix rewrite rule for for `lighttpd` default config [#721](https://github.com/getgrav/grav/pull/2721)
+
+# v1.6.17
+## 11/06/2019
+
+1. [](#new)
+    * Added working ETag (304 Not Modified) support based on the final rendered HTML
+1. [](#improved)
+    * Safer file handling + customizable null char replacement in `CsvFormatter::decode()`
+    * Change of Behavior: `Inflector::hyphenize` will now automatically trim dashes at beginning and end of a string.
+    * Change in Behavior for `Folder::all()` so no longer fails if trying to copy non-existent dot file [#2581](https://github.com/getgrav/grav/pull/2581)
+    * renamed composer `test-plugins` script to `phpstan-plugins` to be more explicit [#2637](https://github.com/getgrav/grav/pull/2637)
+1. [](#bugfix)
+    * Fixed PHP 7.1 bug in FlexMedia
+    * Fix cache image generation when using cropResize [#2639](https://github.com/getgrav/grav/pull/2639)
+    * Fix `array_merge()` exception with non-array page header metadata [#2701](https://github.com/getgrav/grav/pull/2701)
+
 # v1.6.16
 ## 09/19/2019
 
@@ -35,7 +159,7 @@
     * Support new GRAV_BASEDIR environment variable [#2541](https://github.com/getgrav/grav/pull/2541)
     * Allow users to override plugin handler priorities [#2165](https://github.com/getgrav/grav/pull/2165)
 1. [](#improved)
-    * Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)  
+    * Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)
     * Updated vendor libraries
     * Markdown filter is now page-aware so that it works with modular references [admin#1731](https://github.com/getgrav/grav-plugin-admin/issues/1731)
     * Check of `GRAV_USER_INSTANCE` constant is already defined [#2621](https://github.com/getgrav/grav/pull/2621)
@@ -54,7 +178,7 @@
     * Fixed `FlexObject::exists()` failing sometimes just after the object has been saved
     * Fixed CSV formatter not encoding strings with `"` and `,` properly
     * Fixed var order in `Validation.php` [#2610](https://github.com/getgrav/grav/issues/2610)
-    
+
 # v1.6.11
 ## 06/21/2019
 
@@ -85,7 +209,7 @@
     * Fixed regression with `bin/plugin` not listing the plugins available (1c725c0)
     * Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518)
     * Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511)
-    * Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511) 
+    * Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511)
     * Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524)
 
 # v1.6.9

+ 8 - 1
bin/gpm

@@ -28,6 +28,13 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
 }
 
+// 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');
+
 if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
@@ -55,7 +62,7 @@ $grav->setup($environment);
 
 $grav['config']->init();
 $grav['uri']->init();
-$grav['users'];
+$grav['accounts'];
 
 $app = new Application('Grav Package Manager', GRAV_VERSION);
 $app->addCommands(array(

+ 11 - 4
bin/grav

@@ -25,6 +25,17 @@ 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 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');
+
 $climate = new League\CLImate\CLImate;
 $climate->arguments->add([
     'environment' => [
@@ -42,10 +53,6 @@ $environment = $climate->arguments->get('environment');
 $grav = Grav::instance(array('loader' => $autoload));
 $grav->setup($environment);
 
-if (!ini_get('date.timezone')) {
-    date_default_timezone_set('UTC');
-}
-
 if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }

+ 8 - 6
bin/plugin

@@ -32,6 +32,13 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
 }
 
+// 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');
+
 if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
@@ -51,12 +58,7 @@ $environment = $climate->arguments->get('environment');
 
 $grav = Grav::instance(array('loader' => $autoload));
 $grav->setup($environment);
-
-$grav['config']->init();
-$grav['uri']->init();
-$grav['users'];
-$grav['plugins']->init();
-$grav['themes']->init();
+$grav->initializeCli();
 
 $app     = new Application('Grav Plugins Commands', GRAV_VERSION);
 $pattern = '([A-Z]\w+Command\.php)';

+ 18 - 13
composer.json

@@ -2,7 +2,13 @@
     "name": "getgrav/grav",
     "type": "project",
     "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS",
-    "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"],
+    "keywords": [
+        "cms",
+        "flat-file cms",
+        "flat cms",
+        "flatfile cms",
+        "php"
+    ],
     "homepage": "https://getgrav.org",
     "license": "MIT",
     "require": {
@@ -16,17 +22,12 @@
         "symfony/polyfill-iconv": "^1.9",
         "symfony/polyfill-php72": "^1.9",
         "symfony/polyfill-php73": "^1.9",
-
         "psr/simple-cache": "^1.0",
         "psr/http-message": "^1.0",
         "psr/http-server-middleware": "^1.0",
-
         "kodus/psr7-server": "*",
         "nyholm/psr7": "^1.0",
-
         "twig/twig": "~1.40",
-        "erusev/parsedown": "1.6.4",
-        "erusev/parsedown-extra": "~0.7",
         "symfony/yaml": "~4.2.0",
         "symfony/console": "~4.2.0",
         "symfony/event-dispatcher": "~4.2.0",
@@ -36,13 +37,12 @@
         "doctrine/collections": "^1.5",
         "guzzlehttp/psr7": "^1.4",
         "filp/whoops": "~2.2",
-
         "matthiasmullie/minify": "^1.3",
         "monolog/monolog": "~1.0",
         "gregwar/image": "2.*",
-        "donatj/phpuseragentparser": "~0.10",
+        "donatj/phpuseragentparser": "~1.0",
         "pimple/pimple": "~3.2",
-        "rockettheme/toolbox": "~1.4",
+        "rockettheme/toolbox": "~1.4.0",
         "maximebf/debugbar": "~1.15",
         "league/climate": "^3.4",
         "antoligy/dom-string-iterators": "^1.0",
@@ -50,7 +50,7 @@
         "composer/ca-bundle": "^1.0",
         "dragonmantank/cron-expression": "^1.2",
         "phive/twig-extensions-deferred": "^1.0",
-        "willdurand/negotiation": "^2.3"
+        "willdurand/negotiation": "2.x-dev"
     },
     "require-dev": {
         "codeception/codeception": "^2.4",
@@ -83,10 +83,15 @@
         "psr-4": {
             "Grav\\": "system/src/Grav"
         },
-        "files": ["system/defines.php"]
+        "files": [
+            "system/defines.php",
+            "system/aliases.php"
+        ]
     },
     "archive": {
-        "exclude": ["VERSION"]
+        "exclude": [
+            "VERSION"
+        ]
     },
     "scripts": {
         "api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md",
@@ -94,7 +99,7 @@
         "post-create-project-cmd": "bin/grav install",
         "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
         "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
-        "test-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M",
+        "phpstan-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M",
         "test": "vendor/bin/codecept run unit",
         "test-windows": "vendor\\bin\\codecept run unit"
     },

File diff suppressed because it is too large
+ 181 - 252
composer.lock


+ 1 - 1
fixperms.sh

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

+ 10 - 9
index.php

@@ -20,6 +20,16 @@ if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
     die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
 }
 
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
+
+// Set internal encoding.
+if (!\extension_loaded('mbstring')) {
+    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
+}
+@ini_set('default_charset', 'UTF-8');
+mb_internal_encoding('UTF-8');
+
 // Ensure vendor libraries exist
 $autoload = __DIR__ . '/vendor/autoload.php';
 if (!is_file($autoload)) {
@@ -32,15 +42,6 @@ $loader = require $autoload;
 use Grav\Common\Grav;
 use RocketTheme\Toolbox\Event\Event;
 
-// 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 mbstring loaded
-if (!\extension_loaded('mbstring')) {
-    die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
-}
-mb_internal_encoding('UTF-8');
-
 // Get the Grav instance
 $grav = Grav::instance(
     array(

+ 5 - 0
system/aliases.php

@@ -0,0 +1,5 @@
+<?php
+
+/** Moved from non-namespaced classes to Grav Framework */
+class_alias(Grav\Framework\Parsedown\Parsedown::class, '\Parsedown');
+class_alias(Grav\Framework\Parsedown\ParsedownExtra::class, '\ParsedownExtra');

File diff suppressed because it is too large
+ 0 - 1
system/assets/jquery/jquery-3.x.min.js


+ 64 - 7
system/blueprints/config/system.yaml

@@ -241,13 +241,15 @@ form:
                                 type: commalist
 
                         pages.hide_empty_folders:
-                            type: selectize
-                            size: large
-                            label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
-                            help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
-                            classes: fancy
-                            validate:
-                                type: commalist
+                          type: toggle
+                          label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
+                          help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
+                          highlight: 0
+                          options:
+                            1: PLUGIN_ADMIN.YES
+                            0: PLUGIN_ADMIN.NO
+                          validate:
+                            type: bool
 
                         pages.url_taxonomy_filters:
                             type: toggle
@@ -513,6 +515,16 @@ form:
                             validate:
                                 type: bool
 
+                        pages.markdown.valid_link_attributes:
+                          type: selectize
+                          size: large
+                          label: PLUGIN_ADMIN.VALID_LINK_ATTRIBUTES
+                          help: PLUGIN_ADMIN.VALID_LINK_ATTRIBUTES_HELP
+                          placeholder: "rel, target, id, class, classes"
+                          classes: fancy
+                          validate:
+                            type: commalist
+
                 caching:
                     type: tab
                     title: PLUGIN_ADMIN.CACHING
@@ -1379,6 +1391,51 @@ form:
                             label: PLUGIN_ADMIN.CUSTOM_BASE_URL
                             help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
 
+                        http_x_forwarded.protocol:
+                          type: toggle
+                          label: HTTP_X_FORWARDED_PROTO Enabled
+                          highlight: 1
+                          default: 1
+                          options:
+                            1: PLUGIN_ADMIN.YES
+                            0: PLUGIN_ADMIN.NO
+                          validate:
+                            type: bool
+
+                        http_x_forwarded.host:
+                          type: toggle
+                          label: HTTP_X_FORWARDED_HOST Enabled
+                          highlight: 0
+                          default: 0
+                          options:
+                            1: PLUGIN_ADMIN.YES
+                            0: PLUGIN_ADMIN.NO
+                          validate:
+                            type: bool
+
+                        http_x_forwarded.port:
+                          type: toggle
+                          label: HTTP_X_FORWARDED_PORT Enabled
+                          highlight: 1
+                          default: 1
+                          options:
+                            1: PLUGIN_ADMIN.YES
+                            0: PLUGIN_ADMIN.NO
+                          validate:
+                            type: bool
+
+                        http_x_forwarded.ip:
+                          type: toggle
+                          label: HTTP_X_FORWARDED IP Enabled
+                          highlight: 1
+                          default: 1
+                          options:
+                            1: PLUGIN_ADMIN.YES
+                            0: PLUGIN_ADMIN.NO
+                          validate:
+                            type: bool
+
+
                         accounts.type:
                             type: hidden
 

+ 11 - 0
system/config/system.yaml

@@ -10,6 +10,11 @@ custom_base_url: ''                              # Set the base_url manually, e.
 username_regex: '^[a-z0-9_-]{3,16}$'             # Only lowercase chars, digits, dashes, underscores. 3 - 16 chars
 pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' # At least one number, one uppercase and lowercase letter, and be at least 8+ chars
 intl_enabled: true                               # Special logic for PHP International Extension (mod_intl)
+http_x_forwarded:                                # Configuration options for the various HTTP_X_FORWARD headers
+  protocol: true
+  host: false
+  port: true
+  ip: true
 
 languages:
   supported: []                                  # List of languages supported. eg: [en, fr, de]
@@ -54,6 +59,12 @@ pages:
     special_chars:                               # List of special characters to automatically convert to entities
       '>': 'gt'
       '<': 'lt'
+    valid_link_attributes:                       # Valid attributes to pass through via markdown links
+      - rel
+      - target
+      - id
+      - class
+      - classes
   types: [html,htm,xml,txt,json,rss,atom]        # list of valid page types
   append_url_extension: ''                       # Append page's extension in Page urls (e.g. '.html' results in /path/page.html)
   expires: 604800                                # Page expires time in seconds (604800 seconds = 7 days)

+ 1 - 1
system/defines.php

@@ -8,7 +8,7 @@
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.6.16');
+define('GRAV_VERSION', '1.6.27');
 define('GRAV_TESTING', false);
 define('DS', '/');
 

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

@@ -51,7 +51,11 @@ trait LegacyAssetsTrait
                 // special case to handle old attributes being passed in
                 if (isset($arguments['attributes'])) {
                     $old_attributes = $arguments['attributes'];
-                    $arguments = array_merge($arguments, $old_attributes);
+                    if (is_array($old_attributes)) {
+                        $arguments = array_merge($arguments, $old_attributes);
+                    } else {
+                        $arguments['type'] = $old_attributes;
+                    }
                 }
                 unset($arguments['attributes']);
 

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

@@ -9,6 +9,8 @@
 
 namespace Grav\Common;
 
+use function donatj\UserAgent\parse_user_agent;
+
 /**
  * Internally uses the PhpUserAgent package https://github.com/donatj/PhpUserAgent
  */

+ 4 - 4
system/src/Grav/Common/Data/BlueprintSchema.php

@@ -136,7 +136,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     {
         $messages = $this->checkRequired($data, $rules);
 
-        foreach ($data as $key => $field) {
+        foreach ($data as $key => $child) {
             $val = $rules[$key] ?? $rules['*'] ?? null;
             $rule = \is_string($val) ? $this->items[$val] : null;
 
@@ -147,10 +147,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                     continue;
                 }
 
-                $messages += Validation::validate($field, $rule);
-            } elseif (\is_array($field) && \is_array($val)) {
+                $messages += Validation::validate($child, $rule);
+            } elseif (\is_array($child) && \is_array($val)) {
                 // Array has been defined in blueprints.
-                $messages += $this->validateArray($field, $val);
+                $messages += $this->validateArray($child, $val);
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
                 // Undefined/extra item.
                 throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));

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

@@ -32,6 +32,12 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
     /** @var File */
     protected $storage;
 
+    /** @var bool */
+    private $missingValuesAsNull = false;
+
+    /** @var bool */
+    private $keepEmptyValues = true;
+
     /**
      * @param array $items
      * @param Blueprint|callable $blueprints
@@ -42,6 +48,28 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
         $this->blueprints = $blueprints;
     }
 
+    /**
+     * @param bool $value
+     * @return $this
+     */
+    public function setKeepEmptyValues(bool $value)
+    {
+        $this->keepEmptyValues = $value;
+
+        return $this;
+    }
+
+    /**
+     * @param bool $value
+     * @return $this
+     */
+    public function setMissingValuesAsNull(bool $value)
+    {
+        $this->missingValuesAsNull = $value;
+
+        return $this;
+    }
+
     /**
      * Get value by using dot notation for nested arrays/objects.
      *
@@ -202,8 +230,8 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
     public function filter()
     {
         $args = func_get_args();
-        $missingValuesAsNull = (bool)(array_shift($args) ?: false);
-        $keepEmptyValues = (bool)(array_shift($args) ?: false);
+        $missingValuesAsNull = (bool)(array_shift($args) ?? $this->missingValuesAsNull);
+        $keepEmptyValues = (bool)(array_shift($args) ?? $this->keepEmptyValues);
 
         $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues);
 

+ 10 - 1
system/src/Grav/Common/Data/Validation.php

@@ -166,9 +166,18 @@ class Validation
         return (string) $value;
     }
 
+    /**
+     * @param mixed $value
+     * @param array $params
+     * @param array $field
+     * @return string|null
+     */
     protected static function filterCheckbox($value, array $params, array $field)
     {
-        return (bool) $value;
+        $value = (string)$value;
+        $field_value = (string)($field['value'] ?? '1');
+
+        return $value === $field_value ? $value : null;
     }
 
     protected static function filterCommaList($value, array $params, array $field)

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

@@ -347,7 +347,7 @@ class Debugger
      */
     public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
     {
-        if ($errno !== E_USER_DEPRECATED) {
+        if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) {
             if ($this->errorHandler) {
                 return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
             }

+ 26 - 12
system/src/Grav/Common/Filesystem/Folder.php

@@ -22,6 +22,10 @@ abstract class Folder
      */
     public static function lastModifiedFolder($path)
     {
+        if (!file_exists($path)) {
+            return 0;
+        }
+
         $last_modified = 0;
 
         /** @var UniformResourceLocator $locator */
@@ -56,6 +60,10 @@ abstract class Folder
      */
     public static function lastModifiedFile($path, $extensions = 'md|yaml')
     {
+        if (!file_exists($path)) {
+            return 0;
+        }
+
         $last_modified = 0;
 
         /** @var UniformResourceLocator $locator */
@@ -92,21 +100,24 @@ abstract class Folder
      */
     public static function hashAllFiles($path)
     {
-        $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
         $files = [];
 
-        /** @var UniformResourceLocator $locator */
-        $locator = Grav::instance()['locator'];
-        if ($locator->isStream($path)) {
-            $directory = $locator->getRecursiveIterator($path, $flags);
-        } else {
-            $directory = new \RecursiveDirectoryIterator($path, $flags);
-        }
+        if (file_exists($path)) {
+            $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
 
-        $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
+            /** @var UniformResourceLocator $locator */
+            $locator = Grav::instance()['locator'];
+            if ($locator->isStream($path)) {
+                $directory = $locator->getRecursiveIterator($path, $flags);
+            } else {
+                $directory = new \RecursiveDirectoryIterator($path, $flags);
+            }
 
-        foreach ($iterator as $file) {
-            $files[] = $file->getPathname() . '?'. $file->getMTime();
+            $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
+
+            foreach ($iterator as $file) {
+                $files[] = $file->getPathname() . '?'. $file->getMTime();
+            }
         }
 
         return md5(serialize($files));
@@ -199,6 +210,9 @@ abstract class Folder
         if ($path === false) {
             throw new \RuntimeException("Path doesn't exist.");
         }
+        if (!file_exists($path)) {
+            return [];
+        }
 
         $compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
         $pattern = $params['pattern'] ?? null;
@@ -235,7 +249,7 @@ abstract class Folder
         /** @var \RecursiveDirectoryIterator $file */
         foreach ($iterator as $file) {
             // Ignore hidden files.
-            if (strpos($file->getFilename(), '.') === 0) {
+            if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
                 continue;
             }
             if (!$folders && $file->isDir()) {

+ 2 - 1
system/src/Grav/Common/GPM/Local/Package.php

@@ -11,6 +11,7 @@ namespace Grav\Common\GPM\Local;
 
 use Grav\Common\Data\Data;
 use Grav\Common\GPM\Common\Package as BasePackage;
+use Grav\Framework\Parsedown\Parsedown;
 
 class Package extends BasePackage
 {
@@ -23,7 +24,7 @@ class Package extends BasePackage
 
         $this->settings = $package->toArray();
 
-        $html_description = \Parsedown::instance()->line($this->__get('description'));
+        $html_description = Parsedown::instance()->line($this->__get('description'));
         $this->data->set('slug', $package->__get('slug'));
         $this->data->set('description_html', $html_description);
         $this->data->set('description_plain', strip_tags($html_description));

+ 1 - 1
system/src/Grav/Common/GPM/Response.php

@@ -409,7 +409,7 @@ class Response
             } else {
                 $code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
                 if ($code === 301 || $code === 302 || $code === 303) {
-                    preg_match('/Location:(.*?)\n/', $header, $matches);
+                    preg_match('/(?:^|\n)Location:(.*?)\n/i', $header, $matches);
                     $uri = trim(array_pop($matches));
                 } else {
                     $code = 0;

+ 47 - 4
system/src/Grav/Common/Grav.php

@@ -170,6 +170,29 @@ class Grav extends Container
         return $this;
     }
 
+    /**
+     * Initialize CLI environment.
+     *
+     * Call after `$grav->setup($environment)`
+     *
+     * - Load configuration
+     * - Disable debugger
+     * - Set timezone, locale
+     * - Load plugins
+     * - Set Users type to be used in the site
+     *
+     * This method WILL NOT initialize assets, twig or pages.
+     *
+     * @param string|null $environment
+     * @return $this
+     */
+    public function initializeCli()
+    {
+        InitializeProcessor::initializeCli($this);
+
+        return $this;
+    }
+
     /**
      * Process a request
      */
@@ -247,9 +270,21 @@ class Grav extends Container
         $collection = new RequestHandler($this->middleware, $default, $container);
 
         $response = $collection->handle($this['request']);
+        $body = $response->getBody();
+
+        // Handle ETag and If-None-Match headers.
+        if ($response->getHeaderLine('ETag') === '1') {
+            $etag = md5($body);
+            $response = $response->withHeader('ETag', $etag);
+
+            if ($this['request']->getHeaderLine('If-None-Match') === $etag) {
+                $response = $response->withStatus(304);
+                $body = '';
+            }
+        }
 
         $this->header($response);
-        echo $response->getBody();
+        echo $body;
 
         $debugger->render();
 
@@ -281,7 +316,10 @@ class Grav extends Container
         /** @var Uri $uri */
         $uri = $this['uri'];
 
-        //Check for code in route
+        // Clean route for redirect
+        $route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
+
+         // Check for code in route
         $regex = '/.*(\[(30[1-7])\])$/';
         preg_match($regex, $route, $matches);
         if ($matches) {
@@ -427,11 +465,16 @@ class Grav extends Container
      * Used to call closures.
      *
      * Source: http://stackoverflow.com/questions/419804/closures-as-class-members
+     *
+     * @param string $method
+     * @param array $args
+     * @return
      */
     public function __call($method, $args)
     {
-        $closure = $this->{$method};
-        \call_user_func_array($closure, $args);
+        $closure = $this->{$method} ?? null;
+
+        return is_callable($closure) ? $closure(...$args) : null;
     }
 
     /**

+ 2 - 0
system/src/Grav/Common/Inflector.php

@@ -193,6 +193,8 @@ class Inflector
         $regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2);
         $regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3);
 
+        $regex4 = trim($regex4, '-');
+
         return strtolower($regex4);
     }
 

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

@@ -11,8 +11,9 @@ namespace Grav\Common\Markdown;
 
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Markdown\Excerpts;
+use Grav\Framework\Parsedown\Parsedown as ParsedownLib;
 
-class Parsedown extends \Parsedown
+class Parsedown extends ParsedownLib
 {
     use ParsedownGravTrait;
 

+ 2 - 1
system/src/Grav/Common/Markdown/ParsedownExtra.php

@@ -11,8 +11,9 @@ namespace Grav\Common\Markdown;
 
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Markdown\Excerpts;
+use Grav\Framework\Parsedown\ParsedownExtra as ParsedownExtraLib;
 
-class ParsedownExtra extends \ParsedownExtra
+class ParsedownExtra extends ParsedownExtraLib
 {
     use ParsedownGravTrait;
 

+ 1 - 1
system/src/Grav/Common/Media/Traits/MediaTrait.php

@@ -125,5 +125,5 @@ trait MediaTrait
     /**
      * @return string
      */
-    abstract protected function getCacheKey();
+    abstract protected function getCacheKey(): string;
 }

+ 2 - 1
system/src/Grav/Common/Page/Markdown/Excerpts.php

@@ -87,7 +87,7 @@ class Excerpts
             );
 
             // Valid attributes supported.
-            $valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
+            $valid_attributes = Grav::instance()['config']->get('system.pages.markdown.valid_link_attributes');
 
             // Unless told to not process, go through actions.
             if (array_key_exists('noprocess', $actions)) {
@@ -232,6 +232,7 @@ class Excerpts
         $url_parts = is_string($url) ? $this->parseUrl($url) : $url;
         $actions = [];
 
+
         // if there is a query, then parse it and build action calls
         if (isset($url_parts['query'])) {
             $actions = array_reduce(

+ 9 - 4
system/src/Grav/Common/Page/Medium/ImageMedium.php

@@ -516,6 +516,15 @@ class ImageMedium extends Medium
         return $this;
     }
 
+    /**
+     * Handle this commonly used variant
+     */
+    public function cropZoom()
+    {
+        $this->__call('zoomCrop', func_get_args());
+        return $this;
+    }
+
     /**
      * Forward the call to the image processing method.
      *
@@ -525,10 +534,6 @@ class ImageMedium extends Medium
      */
     public function __call($method, $args)
     {
-        if ($method === 'cropZoom') {
-            $method = 'zoomCrop';
-        }
-
         if (!\in_array($method, self::$magic_actions, true)) {
             return parent::__call($method, $args);
         }

+ 14 - 10
system/src/Grav/Common/Page/Page.php

@@ -529,9 +529,9 @@ class Page implements PageInterface
             $headers['Last-Modified'] = $last_modified_date;
         }
 
-        // Calculate ETag based on the raw file
+        // Ask Grav to calculate ETag from the final content.
         if ($this->eTag()) {
-            $headers['ETag'] = '"' . md5($this->raw() . $this->modified()).'"';
+            $headers['ETag'] = '1';
         }
 
         // Set Vary: Accept-Encoding header
@@ -608,12 +608,12 @@ class Page implements PageInterface
                 return $content;
             }
 
-            return mb_strimwidth($content, 0, $size, '...', 'utf-8');
+            return mb_strimwidth($content, 0, $size, '…', 'UTF-8');
         }
 
         $summary = Utils::truncateHtml($content, $size);
 
-        return html_entity_decode($summary);
+        return html_entity_decode($summary, ENT_COMPAT | ENT_HTML401, 'UTF-8');
     }
 
     /**
@@ -659,7 +659,7 @@ class Page implements PageInterface
             // Load cached content
             /** @var Cache $cache */
             $cache = Grav::instance()['cache'];
-            $cache_id = md5('page' . $this->id());
+            $cache_id = md5('page' . $this->getCacheKey());
             $content_obj = $cache->fetch($cache_id);
 
             if (is_array($content_obj)) {
@@ -865,7 +865,7 @@ class Page implements PageInterface
     public function cachePageContent()
     {
         $cache = Grav::instance()['cache'];
-        $cache_id = md5('page' . $this->id());
+        $cache_id = md5('page' . $this->getCacheKey());
         $cache->save($cache_id, ['content' => $this->content, 'content_meta' => $this->content_meta]);
     }
 
@@ -1200,7 +1200,7 @@ class Page implements PageInterface
     /**
      * @return string
      */
-    protected function getCacheKey()
+    public function getCacheKey(): string
     {
         return $this->id();
     }
@@ -1694,9 +1694,9 @@ class Page implements PageInterface
             $metadata['generator'] = 'GravCMS';
 
             // Get initial metadata for the page
-            $metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata'));
+            $metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata', []));
 
-            if (isset($this->header->metadata)) {
+            if (isset($this->header->metadata) && is_array($this->header->metadata)) {
                 // Merge any site.metadata settings in with page metadata
                 $metadata = array_merge($metadata, $this->header->metadata);
             }
@@ -2009,6 +2009,10 @@ class Page implements PageInterface
      */
     public function id($var = null)
     {
+        if (null === $this->id) {
+            // We need to set unique id to avoid potential cache conflicts between pages.
+            $var = time() . md5($this->filePath());
+        }
         if ($var !== null) {
             // store unique per language
             $active_lang = Grav::instance()['language']->getLanguage() ?: '';
@@ -2824,7 +2828,7 @@ class Page implements PageInterface
         if ($pagination) {
             $params = $collection->params();
 
-            $limit = $params['limit'] ?? 0;
+            $limit = (int)($params['limit'] ?? 0);
             $start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0;
 
             if ($limit && $collection->count() > $limit) {

+ 42 - 22
system/src/Grav/Common/Page/Pages.php

@@ -95,6 +95,8 @@ class Pages
 
     protected $initialized = false;
 
+    protected $active_lang;
+
     /**
      * @var Types
      */
@@ -143,7 +145,7 @@ class Pages
      */
     public function baseRoute($lang = null)
     {
-        $key = $lang ?: 'default';
+        $key = $lang ?: $this->active_lang ?: 'default';
 
         if (!isset($this->baseRoute[$key])) {
             /** @var Language $language */
@@ -236,6 +238,16 @@ class Pages
         $this->check_method = strtolower($method);
     }
 
+    /**
+     * Reset pages (used in search indexing etc).
+     */
+    public function reset()
+    {
+        $this->initialized = false;
+
+        $this->init();
+    }
+
     /**
      * Class initialization. Must be called before using this class.
      */
@@ -958,6 +970,9 @@ class Pages
 
         $pages_dir = $locator->findResource('page://');
 
+        // Set active language
+        $this->active_lang = $language->getActive();
+
         if ($config->get('system.cache.enabled')) {
             /** @var Cache $cache */
             $cache = $this->grav['cache'];
@@ -982,17 +997,19 @@ class Pages
 
             $this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
 
-            list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($this->pages_cache_id);
-            if (!$this->instances) {
-                $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
+            $cached = $cache->fetch($this->pages_cache_id);
+            if ($cached) {
+                $this->grav['debugger']->addMessage('Page cache hit.');
 
-                // recurse pages and cache result
-                $this->resetPages($pages_dir);
+                list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cached;
 
-            } else {
                 // If pages was found in cache, set the taxonomy
-                $this->grav['debugger']->addMessage('Page cache hit.');
                 $taxonomy->taxonomy($taxonomy_map);
+            } else {
+                $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
+
+                // recurse pages and cache result
+                $this->resetPages($pages_dir);
             }
         } else {
             $this->recurse($pages_dir);
@@ -1261,14 +1278,13 @@ class Pages
     {
         $list = [];
         $header_default = null;
-        $header_query = null;
+        $header_query = [];
 
         // do this header query work only once
         if (strpos($order_by, 'header.') === 0) {
-            $header_query = explode('|', str_replace('header.', '', $order_by));
-            if (isset($header_query[1])) {
-                $header_default = $header_query[1];
-            }
+            $query = explode('|', str_replace('header.', '', $order_by), 2);
+            $header_query = array_shift($query) ?? '';
+            $header_default = array_shift($query);
         }
 
         foreach ($pages as $key => $info) {
@@ -1306,11 +1322,17 @@ class Pages
                 case 'folder':
                     $list[$key] = $child->folder();
                     break;
-                case (is_string($header_query[0])):
-                    $child_header = new Header((array)$child->header());
-                    $header_value = $child_header->get($header_query[0]);
+                case 'manual':
+                case 'default':
+                default:
+                if (is_string($header_query)) {
+                    $child_header = $child->header();
+                    if (!$child_header instanceof Header) {
+                        $child_header = new Header((array)$child_header);
+                    }
+                    $header_value = $child_header->get($header_query);
                     if (is_array($header_value)) {
-                        $list[$key] = implode(',',$header_value);
+                        $list[$key] = implode(',', $header_value);
                     } elseif ($header_value) {
                         $list[$key] = $header_value;
                     } else {
@@ -1318,11 +1340,9 @@ class Pages
                     }
                     $sort_flags = $sort_flags ?: SORT_REGULAR;
                     break;
-                case 'manual':
-                case 'default':
-                default:
-                    $list[$key] = $key;
-                    $sort_flags = $sort_flags ?: SORT_REGULAR;
+                }
+                $list[$key] = $key;
+                $sort_flags = $sort_flags ?: SORT_REGULAR;
             }
         }
 

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

@@ -305,7 +305,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
 
             // Create new config object and set it on the page object so it's cached for next time
             $page->modifyHeader($class_name_merged, new Data($header));
-        } else if (isset($page_header->{$class_name_merged})) {
+        } elseif (isset($page_header->{$class_name_merged})) {
             $merged = $page_header->{$class_name_merged};
             $header = $merged->toArray();
         }

+ 48 - 0
system/src/Grav/Common/Processors/InitializeProcessor.php

@@ -10,6 +10,7 @@
 namespace Grav\Common\Processors;
 
 use Grav\Common\Config\Config;
+use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
 use Grav\Framework\Session\Exceptions\SessionException;
@@ -22,6 +23,22 @@ class InitializeProcessor extends ProcessorBase
     public $id = 'init';
     public $title = 'Initialize';
 
+    /** @var bool */
+    private static $cli_initialized = false;
+
+    /**
+     * @param Grav $grav
+     */
+    public static function initializeCli(Grav $grav)
+    {
+        if (!static::$cli_initialized) {
+            static::$cli_initialized = true;
+
+            $instance = new static($grav);
+            $instance->processCli();
+        }
+    }
+
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
     {
         $this->startTimer();
@@ -77,4 +94,35 @@ class InitializeProcessor extends ProcessorBase
 
         return $handler->handle($request);
     }
+
+    public function processCli(): void
+    {
+        // Load configuration.
+        $this->container['config']->init();
+        $this->container['plugins']->setup();
+
+        // Disable debugger.
+        $this->container['debugger']->enabled(false);
+
+        // Set timezone, locale.
+        /** @var Config $config */
+        $config = $this->container['config'];
+        $timezone = $config->get('system.timezone');
+        if ($timezone) {
+            date_default_timezone_set($timezone);
+        }
+        $this->container->setLocale();
+
+        // Load plugins.
+        $this->container['plugins']->init();
+
+        // Initialize URI.
+        /** @var Uri $uri */
+        $uri = $this->container['uri'];
+        $uri->init();
+
+        // Load accounts.
+        // TODO: remove in 2.0.
+        $this->container['accounts'];
+    }
 }

+ 2 - 2
system/src/Grav/Common/Scheduler/Cron.php

@@ -69,7 +69,7 @@ class Cron
             'name_year' => 'année',
             'text_period' => 'Chaque %s',
             'text_mins' => 'à %s minutes',
-            'text_time' => 'à %s:%s',
+            'text_time' => 'à %02s:%02s',
             'text_dow' => 'le %s',
             'text_month' => 'de %s',
             'text_dom' => 'le %s',
@@ -86,7 +86,7 @@ class Cron
             'name_year' => 'year',
             'text_period' => 'Every %s',
             'text_mins' => 'at %s minutes past the hour',
-            'text_time' => 'at %s:%s',
+            'text_time' => 'at %02s:%02s',
             'text_dow' => 'on %s',
             'text_month' => 'of %s',
             'text_dom' => 'on the %s',

+ 13 - 0
system/src/Grav/Common/Service/PagesServiceProvider.php

@@ -26,6 +26,19 @@ class PagesServiceProvider implements ServiceProviderInterface
             return new Pages($c);
         };
 
+        if (\defined('GRAV_CLI')) {
+            $container['page'] = static function ($c) {
+                $path = $c['locator']->findResource('system://pages/notfound.md');
+                $page = new Page();
+                $page->init(new \SplFileInfo($path));
+                $page->routable(false);
+
+                return $page;
+            };
+
+            return;
+        }
+
         $container['page'] = function ($c) {
             /** @var Grav $c */
 

+ 1 - 1
system/src/Grav/Common/Twig/TwigExtension.php

@@ -1055,7 +1055,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
      */
     public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0)
     {
-        return json_decode(html_entity_decode($str), $assoc, $depth, $options);
+        return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options);
     }
 
     /**

+ 22 - 17
system/src/Grav/Common/Uri.php

@@ -151,7 +151,7 @@ class Uri
 
         $this->url = $this->base . $this->uri;
 
-        $uri = str_replace(static::filterPath($this->root), '', $this->url);
+        $uri = Utils::replaceFirstOccurrence(static::filterPath($this->root), '', $this->url);
 
         // remove the setup.php based base if set:
         $setup_base = $grav['pages']->base();
@@ -195,7 +195,7 @@ class Uri
         // set the new url
         $this->url = $this->root . $path;
         $this->path = static::cleanPath($path);
-        $this->content_path = trim(str_replace($this->base, '', $this->path), '/');
+        $this->content_path = trim(Utils::replaceFirstOccurrence($this->base, '', $this->path), '/');
         if ($this->content_path !== '') {
             $this->paths = explode('/', $this->content_path);
         }
@@ -306,7 +306,7 @@ class Uri
     public function param($id)
     {
         if (isset($this->params[$id])) {
-            return html_entity_decode(rawurldecode($this->params[$id]));
+            return html_entity_decode(rawurldecode($this->params[$id]), ENT_COMPAT | ENT_HTML401, 'UTF-8');
         }
 
         return false;
@@ -340,7 +340,7 @@ class Uri
             return $this->url;
         }
 
-        $url = str_replace($this->base, '', rtrim($this->url, '/'));
+        $url = Utils::replaceFirstOccurrence($this->base, '', rtrim($this->url, '/'));
 
         return $url ?: '/';
     }
@@ -489,7 +489,7 @@ class Uri
             return $this->uri;
         }
 
-        return str_replace($this->root_path, '', $this->uri);
+        return Utils::replaceFirstOccurrence($this->root_path, '', $this->uri);
     }
 
     /**
@@ -531,7 +531,7 @@ class Uri
             return $this->root;
         }
 
-        return str_replace($this->base, '', $this->root);
+        return Utils::replaceFirstOccurrence($this->base, '', $this->root);
     }
 
     /**
@@ -541,7 +541,9 @@ class Uri
      */
     public function currentPage()
     {
-        return $this->params['page'] ?? 1;
+        $page = (int)($this->params['page'] ?? 1);
+
+        return max(1, $page);
     }
 
     /**
@@ -629,9 +631,9 @@ class Uri
     {
         if (getenv('HTTP_CLIENT_IP')) {
             $ip = getenv('HTTP_CLIENT_IP');
-        } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
+        } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
             $ip = getenv('HTTP_X_FORWARDED_FOR');
-        } elseif (getenv('HTTP_X_FORWARDED')) {
+        } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
             $ip = getenv('HTTP_X_FORWARDED');
         } elseif (getenv('HTTP_FORWARDED_FOR')) {
             $ip = getenv('HTTP_FORWARDED_FOR');
@@ -783,7 +785,7 @@ class Uri
             }
 
             // special check to see if path checking is required.
-            $just_path = str_replace($normalized_url, '', $normalized_path);
+            $just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path);
             if ($normalized_url === '/' || $just_path === $page->path()) {
                 $url_path = $normalized_url;
             } else {
@@ -852,7 +854,7 @@ class Uri
             }
 
             // strip base from this path
-            $target_path = str_replace($uri->rootUrl(), '', $target_path);
+            $target_path = Utils::replaceFirstOccurrence($uri->rootUrl(), '', $target_path);
 
             // set to / if root
             if (empty($target_path)) {
@@ -877,7 +879,7 @@ class Uri
 
         // Handle route only
         if ($route_only) {
-            $url_path = str_replace(static::filterPath($base_url), '', $url_path);
+            $url_path = Utils::replaceFirstOccurrence(static::filterPath($base_url), '', $url_path);
         }
 
         // transform back to string/array as needed
@@ -998,7 +1000,7 @@ class Uri
         }
 
         // special check to see if path checking is required.
-        $just_path = str_replace($normalized_url, '', $normalized_path);
+        $just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path);
         if ($just_path === $page->path()) {
             return $normalized_url;
         }
@@ -1148,7 +1150,7 @@ class Uri
     protected function createFromEnvironment(array $env)
     {
         // Build scheme.
-        if (isset($env['HTTP_X_FORWARDED_PROTO'])) {
+        if (isset($env['HTTP_X_FORWARDED_PROTO']) && Grav::instance()['config']->get('system.http_x_forwarded.protocol')) {
             $this->scheme = $env['HTTP_X_FORWARDED_PROTO'];
         } elseif (isset($env['X-FORWARDED-PROTO'])) {
             $this->scheme = $env['X-FORWARDED-PROTO'];
@@ -1166,11 +1168,14 @@ class Uri
         $this->password = $env['PHP_AUTH_PW'] ?? null;
 
         // Build host.
-        $hostname = 'localhost';
-        if (isset($env['HTTP_HOST'])) {
+        if (isset($env['HTTP_X_FORWARDED_HOST']) && Grav::instance()['config']->get('system.http_x_forwarded.host')) {
+            $hostname = $env['HTTP_X_FORWARDED_HOST'];
+        } else if (isset($env['HTTP_HOST'])) {
             $hostname = $env['HTTP_HOST'];
         } elseif (isset($env['SERVER_NAME'])) {
             $hostname = $env['SERVER_NAME'];
+        } else {
+            $hostname = 'localhost';
         }
         // Remove port from HTTP_HOST generated $hostname
         $hostname = Utils::substrToString($hostname, ':');
@@ -1178,7 +1183,7 @@ class Uri
         $this->host = $this->validateHostname($hostname) ? $hostname : 'unknown';
 
         // Build port.
-        if (isset($env['HTTP_X_FORWARDED_PORT'])) {
+        if (isset($env['HTTP_X_FORWARDED_PORT']) && Grav::instance()['config']->get('system.http_x_forwarded.port')) {
            $this->port = (int)$env['HTTP_X_FORWARDED_PORT'];
         } elseif (isset($env['X-FORWARDED-PORT'])) {
            $this->port = (int)$env['X-FORWARDED-PORT'];

+ 140 - 1
system/src/Grav/Console/ConsoleCommand.php

@@ -10,6 +10,10 @@
 namespace Grav\Console;
 
 use Grav\Common\Grav;
+use Grav\Common\Language\Language;
+use Grav\Common\Page\Page;
+use Grav\Common\Processors\InitializeProcessor;
+use RocketTheme\Toolbox\Event\Event;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -18,6 +22,13 @@ class ConsoleCommand extends Command
 {
     use ConsoleTrait;
 
+    /** @var bool */
+    private $plugins_initialized = false;
+    /** @var bool */
+    private $themes_initialized = false;
+    /** @var bool */
+    private $pages_initialized = false;
+
     /**
      * @param InputInterface  $input
      * @param OutputInterface $output
@@ -31,12 +42,140 @@ class ConsoleCommand extends Command
     }
 
     /**
-     *
+     * Override with your implementation.
      */
     protected function serve()
     {
     }
 
+    /**
+     * Initialize Grav.
+     *
+     * - Load configuration
+     * - Disable debugger
+     * - Set timezone, locale
+     * - Load plugins
+     * - Set Users type to be used in the site
+     *
+     * Safe to be called multiple times.
+     *
+     * @return $this
+     */
+    final protected function initializeGrav()
+    {
+        InitializeProcessor::initializeCli(Grav::instance());
+
+        return $this;
+    }
+
+    /**
+     * Set language to be used in CLI.
+     *
+     * @param string|null $code
+     */
+    final protected function setLanguage(string $code = null)
+    {
+        $this->initializeGrav();
+
+        $grav = Grav::instance();
+        /** @var Language $language */
+        $language = $grav['language'];
+        if ($language->enabled()) {
+            if ($code && $language->validate($code)) {
+                $language->setActive($code);
+            } else {
+                $language->setActive($language->getDefault());
+            }
+        }
+    }
+
+    /**
+     * Properly initialize plugins.
+     *
+     * - call $this->initializeGrav()
+     * - call onPluginsInitialized event
+     *
+     * Safe to be called multiple times.
+     *
+     * @return $this
+     */
+    final protected function initializePlugins()
+    {
+        if (!$this->plugins_initialized) {
+            $this->plugins_initialized = true;
+
+            $this->initializeGrav();
+
+            // Initialize plugins.
+            $grav = Grav::instance();
+            $grav->fireEvent('onPluginsInitialized');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Properly initialize themes.
+     *
+     * - call $this->initializePlugins()
+     * - initialize theme (call onThemeInitialized event)
+     *
+     * Safe to be called multiple times.
+     *
+     * @return $this
+     */
+    final protected function initializeThemes()
+    {
+        if (!$this->themes_initialized) {
+            $this->themes_initialized = true;
+
+            $this->initializePlugins();
+
+            // Initialize themes.
+            $grav = Grav::instance();
+            $grav['themes']->init();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Properly initialize pages.
+     *
+     * - call $this->initializeThemes()
+     * - initialize assets (call onAssetsInitialized event)
+     * - initialize twig (calls the twig events)
+     * - initialize pages (calls onPagesInitialized event)
+     *
+     * Safe to be called multiple times.
+     *
+     * @return $this
+     */
+    final protected function initializePages()
+    {
+        if (!$this->pages_initialized) {
+            $this->pages_initialized = true;
+
+            $this->initializeThemes();
+
+            $grav = Grav::instance();
+
+            // Initialize assets.
+            $grav['assets']->init();
+            $grav->fireEvent('onAssetsInitialized');
+
+            // Initialize twig.
+            $grav['twig']->init();
+
+            // Initialize pages.
+            $pages = $grav['pages'];
+            $pages->init();
+            $grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages]));
+        }
+
+        return $this;
+    }
+
     protected function displayGPMRelease()
     {
         $this->output->writeln('');

+ 1 - 1
system/src/Grav/Console/Gpm/SelfupgradeCommand.php

@@ -177,7 +177,7 @@ class SelfupgradeCommand extends ConsoleCommand
     }
 
     /**
-     * @param Package $package
+     * @param array $package
      *
      * @return string
      */

+ 20 - 2
system/src/Grav/Framework/File/Formatter/CsvFormatter.php

@@ -79,10 +79,28 @@ class CsvFormatter extends AbstractFormatter
         // Get the field names
         $header = str_getcsv(array_shift($lines), $delimiter);
 
+        // Allow for replacing a null string with null/empty value
+        $null_replace = $this->getConfig('null');
+
         // Get the data
         $list = [];
-        foreach ($lines as $line) {
-            $list[] = array_combine($header, str_getcsv($line, $delimiter));
+        $line = null;
+        try {
+            foreach ($lines as $line) {
+                if (!empty($line)) {
+                    $csv_line = str_getcsv($line, $delimiter);
+
+                    if ($null_replace) {
+                        array_walk($csv_line, function(&$el) use ($null_replace) {
+                           $el = str_replace($null_replace, "\0", $el);
+                        });
+                    }
+
+                    $list[] = array_combine($header, $csv_line);
+                }
+            }
+        } catch (\Exception $e) {
+            throw new \Exception('Badly formatted CSV line: ' . $line);
         }
 
         return $list;

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

@@ -342,5 +342,5 @@ trait FlexMediaTrait
 
     abstract public function getFlexDirectory(): FlexDirectory;
 
-    abstract public function getStorageKey();
+    abstract public function getStorageKey(): string;
 }

+ 1554 - 0
system/src/Grav/Framework/Parsedown/Parsedown.php

@@ -0,0 +1,1554 @@
+<?php
+
+/**
+ * @package    Grav\Framework\Parsedown
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Parsedown;
+
+/*
+ * Parsedown
+ * http://parsedown.org
+ *
+ * (c) Emanuil Rusev
+ * http://erusev.com
+ *
+ * This file ported from officiall Parsedown repo and kept for compatibility.
+ */
+
+class Parsedown
+{
+    # ~
+
+    const version = '1.6.0';
+
+    # ~
+
+    function text($text)
+    {
+        # make sure no definitions are set
+        $this->DefinitionData = array();
+
+        # standardize line breaks
+        $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+        # remove surrounding line breaks
+        $text = trim($text, "\n");
+
+        # split text into lines
+        $lines = explode("\n", $text);
+
+        # iterate through lines to identify blocks
+        $markup = $this->lines($lines);
+
+        # trim line breaks
+        $markup = trim($markup, "\n");
+
+        return $markup;
+    }
+
+    #
+    # Setters
+    #
+
+    function setBreaksEnabled($breaksEnabled)
+    {
+        $this->breaksEnabled = $breaksEnabled;
+
+        return $this;
+    }
+
+    protected $breaksEnabled;
+
+    function setMarkupEscaped($markupEscaped)
+    {
+        $this->markupEscaped = $markupEscaped;
+
+        return $this;
+    }
+
+    protected $markupEscaped;
+
+    function setUrlsLinked($urlsLinked)
+    {
+        $this->urlsLinked = $urlsLinked;
+
+        return $this;
+    }
+
+    protected $urlsLinked = true;
+
+    #
+    # Lines
+    #
+
+    protected $BlockTypes = array(
+        '#' => array('Header'),
+        '*' => array('Rule', 'List'),
+        '+' => array('List'),
+        '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+        '0' => array('List'),
+        '1' => array('List'),
+        '2' => array('List'),
+        '3' => array('List'),
+        '4' => array('List'),
+        '5' => array('List'),
+        '6' => array('List'),
+        '7' => array('List'),
+        '8' => array('List'),
+        '9' => array('List'),
+        ':' => array('Table'),
+        '<' => array('Comment', 'Markup'),
+        '=' => array('SetextHeader'),
+        '>' => array('Quote'),
+        '[' => array('Reference'),
+        '_' => array('Rule'),
+        '`' => array('FencedCode'),
+        '|' => array('Table'),
+        '~' => array('FencedCode'),
+    );
+
+    # ~
+
+    protected $unmarkedBlockTypes = array(
+        'Code',
+    );
+
+    #
+    # Blocks
+    #
+
+    protected function lines(array $lines)
+    {
+        $CurrentBlock = null;
+
+        foreach ($lines as $line)
+        {
+            if (chop($line) === '')
+            {
+                if (isset($CurrentBlock))
+                {
+                    $CurrentBlock['interrupted'] = true;
+                }
+
+                continue;
+            }
+
+            if (strpos($line, "\t") !== false)
+            {
+                $parts = explode("\t", $line);
+
+                $line = $parts[0];
+
+                unset($parts[0]);
+
+                foreach ($parts as $part)
+                {
+                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+
+                    $line .= str_repeat(' ', $shortage);
+                    $line .= $part;
+                }
+            }
+
+            $indent = 0;
+
+            while (isset($line[$indent]) and $line[$indent] === ' ')
+            {
+                $indent ++;
+            }
+
+            $text = $indent > 0 ? substr($line, $indent) : $line;
+
+            # ~
+
+            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+            # ~
+
+            if (isset($CurrentBlock['continuable']))
+            {
+                $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
+
+                if (isset($Block))
+                {
+                    $CurrentBlock = $Block;
+
+                    continue;
+                }
+                else
+                {
+                    if ($this->isBlockCompletable($CurrentBlock['type']))
+                    {
+                        $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+                    }
+                }
+            }
+
+            # ~
+
+            $marker = $text[0];
+
+            # ~
+
+            $blockTypes = $this->unmarkedBlockTypes;
+
+            if (isset($this->BlockTypes[$marker]))
+            {
+                foreach ($this->BlockTypes[$marker] as $blockType)
+                {
+                    $blockTypes []= $blockType;
+                }
+            }
+
+            #
+            # ~
+
+            foreach ($blockTypes as $blockType)
+            {
+                $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
+
+                if (isset($Block))
+                {
+                    $Block['type'] = $blockType;
+
+                    if ( ! isset($Block['identified']))
+                    {
+                        $Blocks []= $CurrentBlock;
+
+                        $Block['identified'] = true;
+                    }
+
+                    if ($this->isBlockContinuable($blockType))
+                    {
+                        $Block['continuable'] = true;
+                    }
+
+                    $CurrentBlock = $Block;
+
+                    continue 2;
+                }
+            }
+
+            # ~
+
+            if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
+            {
+                $CurrentBlock['element']['text'] .= "\n".$text;
+            }
+            else
+            {
+                $Blocks []= $CurrentBlock;
+
+                $CurrentBlock = $this->paragraph($Line);
+
+                $CurrentBlock['identified'] = true;
+            }
+        }
+
+        # ~
+
+        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+        {
+            $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+        }
+
+        # ~
+
+        $Blocks []= $CurrentBlock;
+
+        unset($Blocks[0]);
+
+        # ~
+
+        $markup = '';
+
+        foreach ($Blocks as $Block)
+        {
+            if (isset($Block['hidden']))
+            {
+                continue;
+            }
+
+            $markup .= "\n";
+            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+        }
+
+        $markup .= "\n";
+
+        # ~
+
+        return $markup;
+    }
+
+    protected function isBlockContinuable($Type)
+    {
+        return method_exists($this, 'block'.$Type.'Continue');
+    }
+
+    protected function isBlockCompletable($Type)
+    {
+        return method_exists($this, 'block'.$Type.'Complete');
+    }
+
+    #
+    # Code
+
+    protected function blockCode($Line, $Block = null)
+    {
+        if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if ($Line['indent'] >= 4)
+        {
+            $text = substr($Line['body'], 4);
+
+            $Block = array(
+                'element' => array(
+                    'name' => 'pre',
+                    'handler' => 'element',
+                    'text' => array(
+                        'name' => 'code',
+                        'text' => $text,
+                    ),
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeContinue($Line, $Block)
+    {
+        if ($Line['indent'] >= 4)
+        {
+            if (isset($Block['interrupted']))
+            {
+                $Block['element']['text']['text'] .= "\n";
+
+                unset($Block['interrupted']);
+            }
+
+            $Block['element']['text']['text'] .= "\n";
+
+            $text = substr($Line['body'], 4);
+
+            $Block['element']['text']['text'] .= $text;
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeComplete($Block)
+    {
+        $text = $Block['element']['text']['text'];
+
+        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+        $Block['element']['text']['text'] = $text;
+
+        return $Block;
+    }
+
+    #
+    # Comment
+
+    protected function blockComment($Line)
+    {
+        if ($this->markupEscaped)
+        {
+            return;
+        }
+
+        if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
+        {
+            $Block = array(
+                'markup' => $Line['body'],
+            );
+
+            if (preg_match('/-->$/', $Line['text']))
+            {
+                $Block['closed'] = true;
+            }
+
+            return $Block;
+        }
+    }
+
+    protected function blockCommentContinue($Line, array $Block)
+    {
+        if (isset($Block['closed']))
+        {
+            return;
+        }
+
+        $Block['markup'] .= "\n" . $Line['body'];
+
+        if (preg_match('/-->$/', $Line['text']))
+        {
+            $Block['closed'] = true;
+        }
+
+        return $Block;
+    }
+
+    #
+    # Fenced Code
+
+    protected function blockFencedCode($Line)
+    {
+        if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
+        {
+            $Element = array(
+                'name' => 'code',
+                'text' => '',
+            );
+
+            if (isset($matches[1]))
+            {
+                $class = 'language-'.$matches[1];
+
+                $Element['attributes'] = array(
+                    'class' => $class,
+                );
+            }
+
+            $Block = array(
+                'char' => $Line['text'][0],
+                'element' => array(
+                    'name' => 'pre',
+                    'handler' => 'element',
+                    'text' => $Element,
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockFencedCodeContinue($Line, $Block)
+    {
+        if (isset($Block['complete']))
+        {
+            return;
+        }
+
+        if (isset($Block['interrupted']))
+        {
+            $Block['element']['text']['text'] .= "\n";
+
+            unset($Block['interrupted']);
+        }
+
+        if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
+        {
+            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
+
+            $Block['complete'] = true;
+
+            return $Block;
+        }
+
+        $Block['element']['text']['text'] .= "\n".$Line['body'];
+
+        return $Block;
+    }
+
+    protected function blockFencedCodeComplete($Block)
+    {
+        $text = $Block['element']['text']['text'];
+
+        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+        $Block['element']['text']['text'] = $text;
+
+        return $Block;
+    }
+
+    #
+    # Header
+
+    protected function blockHeader($Line)
+    {
+        if (isset($Line['text'][1]))
+        {
+            $level = 1;
+
+            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
+            {
+                $level ++;
+            }
+
+            if ($level > 6)
+            {
+                return;
+            }
+
+            $text = trim($Line['text'], '# ');
+
+            $Block = array(
+                'element' => array(
+                    'name' => 'h' . min(6, $level),
+                    'text' => $text,
+                    'handler' => 'line',
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # List
+
+    protected function blockList($Line)
+    {
+        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
+
+        if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
+        {
+            $Block = array(
+                'indent' => $Line['indent'],
+                'pattern' => $pattern,
+                'element' => array(
+                    'name' => $name,
+                    'handler' => 'elements',
+                ),
+            );
+
+            if($name === 'ol')
+            {
+                $listStart = stristr($matches[0], '.', true);
+
+                if($listStart !== '1')
+                {
+                    $Block['element']['attributes'] = array('start' => $listStart);
+                }
+            }
+
+            $Block['li'] = array(
+                'name' => 'li',
+                'handler' => 'li',
+                'text' => array(
+                    $matches[2],
+                ),
+            );
+
+            $Block['element']['text'] []= & $Block['li'];
+
+            return $Block;
+        }
+    }
+
+    protected function blockListContinue($Line, array $Block)
+    {
+        if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
+        {
+            if (isset($Block['interrupted']))
+            {
+                $Block['li']['text'] []= '';
+
+                unset($Block['interrupted']);
+            }
+
+            unset($Block['li']);
+
+            $text = isset($matches[1]) ? $matches[1] : '';
+
+            $Block['li'] = array(
+                'name' => 'li',
+                'handler' => 'li',
+                'text' => array(
+                    $text,
+                ),
+            );
+
+            $Block['element']['text'] []= & $Block['li'];
+
+            return $Block;
+        }
+
+        if ($Line['text'][0] === '[' and $this->blockReference($Line))
+        {
+            return $Block;
+        }
+
+        if ( ! isset($Block['interrupted']))
+        {
+            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+            $Block['li']['text'] []= $text;
+
+            return $Block;
+        }
+
+        if ($Line['indent'] > 0)
+        {
+            $Block['li']['text'] []= '';
+
+            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+            $Block['li']['text'] []= $text;
+
+            unset($Block['interrupted']);
+
+            return $Block;
+        }
+    }
+
+    #
+    # Quote
+
+    protected function blockQuote($Line)
+    {
+        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+        {
+            $Block = array(
+                'element' => array(
+                    'name' => 'blockquote',
+                    'handler' => 'lines',
+                    'text' => (array) $matches[1],
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockQuoteContinue($Line, array $Block)
+    {
+        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+        {
+            if (isset($Block['interrupted']))
+            {
+                $Block['element']['text'] []= '';
+
+                unset($Block['interrupted']);
+            }
+
+            $Block['element']['text'] []= $matches[1];
+
+            return $Block;
+        }
+
+        if ( ! isset($Block['interrupted']))
+        {
+            $Block['element']['text'] []= $Line['text'];
+
+            return $Block;
+        }
+    }
+
+    #
+    # Rule
+
+    protected function blockRule($Line)
+    {
+        if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
+        {
+            $Block = array(
+                'element' => array(
+                    'name' => 'hr'
+                ),
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # Setext
+
+    protected function blockSetextHeader($Line, array $Block = null)
+    {
+        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if (chop($Line['text'], $Line['text'][0]) === '')
+        {
+            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+            return $Block;
+        }
+    }
+
+    #
+    # Markup
+
+    protected function blockMarkup($Line)
+    {
+        if ($this->markupEscaped)
+        {
+            return;
+        }
+
+        if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
+        {
+            $element = strtolower($matches[1]);
+
+            if (in_array($element, $this->textLevelElements))
+            {
+                return;
+            }
+
+            $Block = array(
+                'name' => $matches[1],
+                'depth' => 0,
+                'markup' => $Line['text'],
+            );
+
+            $length = strlen($matches[0]);
+
+            $remainder = substr($Line['text'], $length);
+
+            if (trim($remainder) === '')
+            {
+                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+                {
+                    $Block['closed'] = true;
+
+                    $Block['void'] = true;
+                }
+            }
+            else
+            {
+                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+                {
+                    return;
+                }
+
+                if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
+                {
+                    $Block['closed'] = true;
+                }
+            }
+
+            return $Block;
+        }
+    }
+
+    protected function blockMarkupContinue($Line, array $Block)
+    {
+        if (isset($Block['closed']))
+        {
+            return;
+        }
+
+        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
+        {
+            $Block['depth'] ++;
+        }
+
+        if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
+        {
+            if ($Block['depth'] > 0)
+            {
+                $Block['depth'] --;
+            }
+            else
+            {
+                $Block['closed'] = true;
+            }
+        }
+
+        if (isset($Block['interrupted']))
+        {
+            $Block['markup'] .= "\n";
+
+            unset($Block['interrupted']);
+        }
+
+        $Block['markup'] .= "\n".$Line['body'];
+
+        return $Block;
+    }
+
+    #
+    # Reference
+
+    protected function blockReference($Line)
+    {
+        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
+        {
+            $id = strtolower($matches[1]);
+
+            $Data = array(
+                'url' => $matches[2],
+                'title' => null,
+            );
+
+            if (isset($matches[3]))
+            {
+                $Data['title'] = $matches[3];
+            }
+
+            $this->DefinitionData['Reference'][$id] = $Data;
+
+            $Block = array(
+                'hidden' => true,
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # Table
+
+    protected function blockTable($Line, array $Block = null)
+    {
+        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
+        {
+            $alignments = array();
+
+            $divider = $Line['text'];
+
+            $divider = trim($divider);
+            $divider = trim($divider, '|');
+
+            $dividerCells = explode('|', $divider);
+
+            foreach ($dividerCells as $dividerCell)
+            {
+                $dividerCell = trim($dividerCell);
+
+                if ($dividerCell === '')
+                {
+                    continue;
+                }
+
+                $alignment = null;
+
+                if ($dividerCell[0] === ':')
+                {
+                    $alignment = 'left';
+                }
+
+                if (substr($dividerCell, - 1) === ':')
+                {
+                    $alignment = $alignment === 'left' ? 'center' : 'right';
+                }
+
+                $alignments []= $alignment;
+            }
+
+            # ~
+
+            $HeaderElements = array();
+
+            $header = $Block['element']['text'];
+
+            $header = trim($header);
+            $header = trim($header, '|');
+
+            $headerCells = explode('|', $header);
+
+            foreach ($headerCells as $index => $headerCell)
+            {
+                $headerCell = trim($headerCell);
+
+                $HeaderElement = array(
+                    'name' => 'th',
+                    'text' => $headerCell,
+                    'handler' => 'line',
+                );
+
+                if (isset($alignments[$index]))
+                {
+                    $alignment = $alignments[$index];
+
+                    $HeaderElement['attributes'] = array(
+                        'style' => 'text-align: '.$alignment.';',
+                    );
+                }
+
+                $HeaderElements []= $HeaderElement;
+            }
+
+            # ~
+
+            $Block = array(
+                'alignments' => $alignments,
+                'identified' => true,
+                'element' => array(
+                    'name' => 'table',
+                    'handler' => 'elements',
+                ),
+            );
+
+            $Block['element']['text'] []= array(
+                'name' => 'thead',
+                'handler' => 'elements',
+            );
+
+            $Block['element']['text'] []= array(
+                'name' => 'tbody',
+                'handler' => 'elements',
+                'text' => array(),
+            );
+
+            $Block['element']['text'][0]['text'] []= array(
+                'name' => 'tr',
+                'handler' => 'elements',
+                'text' => $HeaderElements,
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockTableContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted']))
+        {
+            return;
+        }
+
+        if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
+        {
+            $Elements = array();
+
+            $row = $Line['text'];
+
+            $row = trim($row);
+            $row = trim($row, '|');
+
+            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
+
+            foreach ($matches[0] as $index => $cell)
+            {
+                $cell = trim($cell);
+
+                $Element = array(
+                    'name' => 'td',
+                    'handler' => 'line',
+                    'text' => $cell,
+                );
+
+                if (isset($Block['alignments'][$index]))
+                {
+                    $Element['attributes'] = array(
+                        'style' => 'text-align: '.$Block['alignments'][$index].';',
+                    );
+                }
+
+                $Elements []= $Element;
+            }
+
+            $Element = array(
+                'name' => 'tr',
+                'handler' => 'elements',
+                'text' => $Elements,
+            );
+
+            $Block['element']['text'][1]['text'] []= $Element;
+
+            return $Block;
+        }
+    }
+
+    #
+    # ~
+    #
+
+    protected function paragraph($Line)
+    {
+        $Block = array(
+            'element' => array(
+                'name' => 'p',
+                'text' => $Line['text'],
+                'handler' => 'line',
+            ),
+        );
+
+        return $Block;
+    }
+
+    #
+    # Inline Elements
+    #
+
+    protected $InlineTypes = array(
+        '"' => array('SpecialCharacter'),
+        '!' => array('Image'),
+        '&' => array('SpecialCharacter'),
+        '*' => array('Emphasis'),
+        ':' => array('Url'),
+        '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
+        '>' => array('SpecialCharacter'),
+        '[' => array('Link'),
+        '_' => array('Emphasis'),
+        '`' => array('Code'),
+        '~' => array('Strikethrough'),
+        '\\' => array('EscapeSequence'),
+    );
+
+    # ~
+
+    protected $inlineMarkerList = '!"*_&[:<>`~\\';
+
+    #
+    # ~
+    #
+
+    public function line($text)
+    {
+        $markup = '';
+
+        # $excerpt is based on the first occurrence of a marker
+
+        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+        {
+            $marker = $excerpt[0];
+
+            $markerPosition = strpos($text, $marker);
+
+            $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+            foreach ($this->InlineTypes[$marker] as $inlineType)
+            {
+                $Inline = $this->{'inline'.$inlineType}($Excerpt);
+
+                if ( ! isset($Inline))
+                {
+                    continue;
+                }
+
+                # makes sure that the inline belongs to "our" marker
+
+                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+                {
+                    continue;
+                }
+
+                # sets a default inline position
+
+                if ( ! isset($Inline['position']))
+                {
+                    $Inline['position'] = $markerPosition;
+                }
+
+                # the text that comes before the inline
+                $unmarkedText = substr($text, 0, $Inline['position']);
+
+                # compile the unmarked text
+                $markup .= $this->unmarkedText($unmarkedText);
+
+                # compile the inline
+                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+                # remove the examined text
+                $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+                continue 2;
+            }
+
+            # the marker does not belong to an inline
+
+            $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+            $markup .= $this->unmarkedText($unmarkedText);
+
+            $text = substr($text, $markerPosition + 1);
+        }
+
+        $markup .= $this->unmarkedText($text);
+
+        return $markup;
+    }
+
+    #
+    # ~
+    #
+
+    protected function inlineCode($Excerpt)
+    {
+        $marker = $Excerpt['text'][0];
+
+        if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
+        {
+            $text = $matches[2];
+            $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+            $text = preg_replace("/[ ]*\n/", ' ', $text);
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'code',
+                    'text' => $text,
+                ),
+            );
+        }
+    }
+
+    protected function inlineEmailTag($Excerpt)
+    {
+        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
+        {
+            $url = $matches[1];
+
+            if ( ! isset($matches[2]))
+            {
+                $url = 'mailto:' . $url;
+            }
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $matches[1],
+                    'attributes' => array(
+                        'href' => $url,
+                    ),
+                ),
+            );
+        }
+    }
+
+    protected function inlineEmphasis($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]))
+        {
+            return;
+        }
+
+        $marker = $Excerpt['text'][0];
+
+        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+        {
+            $emphasis = 'strong';
+        }
+        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+        {
+            $emphasis = 'em';
+        }
+        else
+        {
+            return;
+        }
+
+        return array(
+            'extent' => strlen($matches[0]),
+            'element' => array(
+                'name' => $emphasis,
+                'handler' => 'line',
+                'text' => $matches[1],
+            ),
+        );
+    }
+
+    protected function inlineEscapeSequence($Excerpt)
+    {
+        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+        {
+            return array(
+                'markup' => $Excerpt['text'][1],
+                'extent' => 2,
+            );
+        }
+    }
+
+    protected function inlineImage($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+        {
+            return;
+        }
+
+        $Excerpt['text']= substr($Excerpt['text'], 1);
+
+        $Link = $this->inlineLink($Excerpt);
+
+        if ($Link === null)
+        {
+            return;
+        }
+
+        $Inline = array(
+            'extent' => $Link['extent'] + 1,
+            'element' => array(
+                'name' => 'img',
+                'attributes' => array(
+                    'src' => $Link['element']['attributes']['href'],
+                    'alt' => $Link['element']['text'],
+                ),
+            ),
+        );
+
+        $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+        unset($Inline['element']['attributes']['href']);
+
+        return $Inline;
+    }
+
+    protected function inlineLink($Excerpt)
+    {
+        $Element = array(
+            'name' => 'a',
+            'handler' => 'line',
+            'text' => null,
+            'attributes' => array(
+                'href' => null,
+                'title' => null,
+            ),
+        );
+
+        $extent = 0;
+
+        $remainder = $Excerpt['text'];
+
+        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+        {
+            $Element['text'] = $matches[1];
+
+            $extent += strlen($matches[0]);
+
+            $remainder = substr($remainder, $extent);
+        }
+        else
+        {
+            return;
+        }
+
+        if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
+        {
+            $Element['attributes']['href'] = $matches[1];
+
+            if (isset($matches[2]))
+            {
+                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+            }
+
+            $extent += strlen($matches[0]);
+        }
+        else
+        {
+            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+            {
+                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+                $definition = strtolower($definition);
+
+                $extent += strlen($matches[0]);
+            }
+            else
+            {
+                $definition = strtolower($Element['text']);
+            }
+
+            if ( ! isset($this->DefinitionData['Reference'][$definition]))
+            {
+                return;
+            }
+
+            $Definition = $this->DefinitionData['Reference'][$definition];
+
+            $Element['attributes']['href'] = $Definition['url'];
+            $Element['attributes']['title'] = $Definition['title'];
+        }
+
+        $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
+
+        return array(
+            'extent' => $extent,
+            'element' => $Element,
+        );
+    }
+
+    protected function inlineMarkup($Excerpt)
+    {
+        if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
+        {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            );
+        }
+
+        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            );
+        }
+
+        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
+        {
+            return array(
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            );
+        }
+    }
+
+    protected function inlineSpecialCharacter($Excerpt)
+    {
+        if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
+        {
+            return array(
+                'markup' => '&amp;',
+                'extent' => 1,
+            );
+        }
+
+        $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
+
+        if (isset($SpecialCharacter[$Excerpt['text'][0]]))
+        {
+            return array(
+                'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
+                'extent' => 1,
+            );
+        }
+    }
+
+    protected function inlineStrikethrough($Excerpt)
+    {
+        if ( ! isset($Excerpt['text'][1]))
+        {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+        {
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'del',
+                    'text' => $matches[1],
+                    'handler' => 'line',
+                ),
+            );
+        }
+    }
+
+    protected function inlineUrl($Excerpt)
+    {
+        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+        {
+            return;
+        }
+
+        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
+        {
+            $Inline = array(
+                'extent' => strlen($matches[0][0]),
+                'position' => $matches[0][1],
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $matches[0][0],
+                    'attributes' => array(
+                        'href' => $matches[0][0],
+                    ),
+                ),
+            );
+
+            return $Inline;
+        }
+    }
+
+    protected function inlineUrlTag($Excerpt)
+    {
+        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
+        {
+            $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => array(
+                    'name' => 'a',
+                    'text' => $url,
+                    'attributes' => array(
+                        'href' => $url,
+                    ),
+                ),
+            );
+        }
+    }
+
+    # ~
+
+    protected function unmarkedText($text)
+    {
+        if ($this->breaksEnabled)
+        {
+            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
+        }
+        else
+        {
+            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
+            $text = str_replace(" \n", "\n", $text);
+        }
+
+        return $text;
+    }
+
+    #
+    # Handlers
+    #
+
+    protected function element(array $Element)
+    {
+        $markup = '<'.$Element['name'];
+
+        if (isset($Element['attributes']))
+        {
+            foreach ($Element['attributes'] as $name => $value)
+            {
+                if ($value === null)
+                {
+                    continue;
+                }
+
+                $markup .= ' '.$name.'="'.$value.'"';
+            }
+        }
+
+        if (isset($Element['text']))
+        {
+            $markup .= '>';
+
+            if (isset($Element['handler']))
+            {
+                $markup .= $this->{$Element['handler']}($Element['text']);
+            }
+            else
+            {
+                $markup .= $Element['text'];
+            }
+
+            $markup .= '</'.$Element['name'].'>';
+        }
+        else
+        {
+            $markup .= ' />';
+        }
+
+        return $markup;
+    }
+
+    protected function elements(array $Elements)
+    {
+        $markup = '';
+
+        foreach ($Elements as $Element)
+        {
+            $markup .= "\n" . $this->element($Element);
+        }
+
+        $markup .= "\n";
+
+        return $markup;
+    }
+
+    # ~
+
+    protected function li($lines)
+    {
+        $markup = $this->lines($lines);
+
+        $trimmedMarkup = trim($markup);
+
+        if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
+        {
+            $markup = $trimmedMarkup;
+            $markup = substr($markup, 3);
+
+            $position = strpos($markup, "</p>");
+
+            $markup = substr_replace($markup, '', $position, 4);
+        }
+
+        return $markup;
+    }
+
+    #
+    # Deprecated Methods
+    #
+
+    function parse($text)
+    {
+        $markup = $this->text($text);
+
+        return $markup;
+    }
+
+    #
+    # Static Methods
+    #
+
+    static function instance($name = 'default')
+    {
+        if (isset(self::$instances[$name]))
+        {
+            return self::$instances[$name];
+        }
+
+        $instance = new static();
+
+        self::$instances[$name] = $instance;
+
+        return $instance;
+    }
+
+    private static $instances = array();
+
+    #
+    # Fields
+    #
+
+    protected $DefinitionData;
+
+    #
+    # Read-Only
+
+    protected $specialCharacters = array(
+        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
+    );
+
+    protected $StrongRegex = array(
+        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
+        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
+    );
+
+    protected $EmRegex = array(
+        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+    );
+
+    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
+
+    protected $voidElements = array(
+        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+    );
+
+    protected $textLevelElements = array(
+        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+        'i', 'rp', 'del', 'code',          'strike', 'marquee',
+        'q', 'rt', 'ins', 'font',          'strong',
+        's', 'tt', 'kbd', 'mark',
+        'u', 'xm', 'sub', 'nobr',
+        'sup', 'ruby',
+        'var', 'span',
+        'wbr', 'time',
+    );
+}

+ 532 - 0
system/src/Grav/Framework/Parsedown/ParsedownExtra.php

@@ -0,0 +1,532 @@
+<?php
+
+/**
+ * @package    Grav\Framework\Parsedown
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Parsedown;
+
+/*
+ * Parsedown Extra
+ * http://parsedown.org
+ *
+ * (c) Emanuil Rusev
+ * http://erusev.com
+ *
+ * This file ported from officiall ParsedownExtra repo and kept for compatibility.
+ */
+
+class ParsedownExtra extends Parsedown
+{
+    # ~
+
+    const version = '0.7.0';
+
+    # ~
+
+    function __construct()
+    {
+        if (parent::version < '1.5.0')
+        {
+            throw new Exception('ParsedownExtra requires a later version of Parsedown');
+        }
+
+        $this->BlockTypes[':'] []= 'DefinitionList';
+        $this->BlockTypes['*'] []= 'Abbreviation';
+
+        # identify footnote definitions before reference definitions
+        array_unshift($this->BlockTypes['['], 'Footnote');
+
+        # identify footnote markers before before links
+        array_unshift($this->InlineTypes['['], 'FootnoteMarker');
+    }
+
+    #
+    # ~
+
+    function text($text)
+    {
+        $markup = parent::text($text);
+
+        # merge consecutive dl elements
+
+        $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
+
+        # add footnotes
+
+        if (isset($this->DefinitionData['Footnote']))
+        {
+            $Element = $this->buildFootnoteElement();
+
+            $markup .= "\n" . $this->element($Element);
+        }
+
+        return $markup;
+    }
+
+    #
+    # Blocks
+    #
+
+    #
+    # Abbreviation
+
+    protected function blockAbbreviation($Line)
+    {
+        if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
+        {
+            $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
+
+            $Block = array(
+                'hidden' => true,
+            );
+
+            return $Block;
+        }
+    }
+
+    #
+    # Footnote
+
+    protected function blockFootnote($Line)
+    {
+        if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
+        {
+            $Block = array(
+                'label' => $matches[1],
+                'text' => $matches[2],
+                'hidden' => true,
+            );
+
+            return $Block;
+        }
+    }
+
+    protected function blockFootnoteContinue($Line, $Block)
+    {
+        if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
+        {
+            return;
+        }
+
+        if (isset($Block['interrupted']))
+        {
+            if ($Line['indent'] >= 4)
+            {
+                $Block['text'] .= "\n\n" . $Line['text'];
+
+                return $Block;
+            }
+        }
+        else
+        {
+            $Block['text'] .= "\n" . $Line['text'];
+
+            return $Block;
+        }
+    }
+
+    protected function blockFootnoteComplete($Block)
+    {
+        $this->DefinitionData['Footnote'][$Block['label']] = array(
+            'text' => $Block['text'],
+            'count' => null,
+            'number' => null,
+        );
+
+        return $Block;
+    }
+
+    #
+    # Definition List
+
+    protected function blockDefinitionList($Line, $Block)
+    {
+        if ( ! isset($Block) or isset($Block['type']))
+        {
+            return;
+        }
+
+        $Element = array(
+            'name' => 'dl',
+            'handler' => 'elements',
+            'text' => array(),
+        );
+
+        $terms = explode("\n", $Block['element']['text']);
+
+        foreach ($terms as $term)
+        {
+            $Element['text'] []= array(
+                'name' => 'dt',
+                'handler' => 'line',
+                'text' => $term,
+            );
+        }
+
+        $Block['element'] = $Element;
+
+        $Block = $this->addDdElement($Line, $Block);
+
+        return $Block;
+    }
+
+    protected function blockDefinitionListContinue($Line, array $Block)
+    {
+        if ($Line['text'][0] === ':')
+        {
+            $Block = $this->addDdElement($Line, $Block);
+
+            return $Block;
+        }
+        else
+        {
+            if (isset($Block['interrupted']) and $Line['indent'] === 0)
+            {
+                return;
+            }
+
+            if (isset($Block['interrupted']))
+            {
+                $Block['dd']['handler'] = 'text';
+                $Block['dd']['text'] .= "\n\n";
+
+                unset($Block['interrupted']);
+            }
+
+            $text = substr($Line['body'], min($Line['indent'], 4));
+
+            $Block['dd']['text'] .= "\n" . $text;
+
+            return $Block;
+        }
+    }
+
+    #
+    # Header
+
+    protected function blockHeader($Line)
+    {
+        $Block = parent::blockHeader($Line);
+
+        if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
+        {
+            $attributeString = $matches[1][0];
+
+            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
+
+            $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
+        }
+
+        return $Block;
+    }
+
+    #
+    # Markup
+
+    protected function blockMarkupComplete($Block)
+    {
+        if ( ! isset($Block['void']))
+        {
+            $Block['markup'] = $this->processTag($Block['markup']);
+        }
+
+        return $Block;
+    }
+
+    #
+    # Setext
+
+    protected function blockSetextHeader($Line, array $Block = null)
+    {
+        $Block = parent::blockSetextHeader($Line, $Block);
+
+        if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
+        {
+            $attributeString = $matches[1][0];
+
+            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
+
+            $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
+        }
+
+        return $Block;
+    }
+
+    #
+    # Inline Elements
+    #
+
+    #
+    # Footnote Marker
+
+    protected function inlineFootnoteMarker($Excerpt)
+    {
+        if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
+        {
+            $name = $matches[1];
+
+            if ( ! isset($this->DefinitionData['Footnote'][$name]))
+            {
+                return;
+            }
+
+            $this->DefinitionData['Footnote'][$name]['count'] ++;
+
+            if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
+            {
+                $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
+            }
+
+            $Element = array(
+                'name' => 'sup',
+                'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
+                'handler' => 'element',
+                'text' => array(
+                    'name' => 'a',
+                    'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
+                    'text' => $this->DefinitionData['Footnote'][$name]['number'],
+                ),
+            );
+
+            return array(
+                'extent' => strlen($matches[0]),
+                'element' => $Element,
+            );
+        }
+    }
+
+    private $footnoteCount = 0;
+
+    #
+    # Link
+
+    protected function inlineLink($Excerpt)
+    {
+        $Link = parent::inlineLink($Excerpt);
+
+        $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';
+
+        if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
+        {
+            $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
+
+            $Link['extent'] += strlen($matches[0]);
+        }
+
+        return $Link;
+    }
+
+    #
+    # ~
+    #
+
+    protected function unmarkedText($text)
+    {
+        $text = parent::unmarkedText($text);
+
+        if (isset($this->DefinitionData['Abbreviation']))
+        {
+            foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
+            {
+                $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/';
+
+                $text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text);
+            }
+        }
+
+        return $text;
+    }
+
+    #
+    # Util Methods
+    #
+
+    protected function addDdElement(array $Line, array $Block)
+    {
+        $text = substr($Line['text'], 1);
+        $text = trim($text);
+
+        unset($Block['dd']);
+
+        $Block['dd'] = array(
+            'name' => 'dd',
+            'handler' => 'line',
+            'text' => $text,
+        );
+
+        if (isset($Block['interrupted']))
+        {
+            $Block['dd']['handler'] = 'text';
+
+            unset($Block['interrupted']);
+        }
+
+        $Block['element']['text'] []= & $Block['dd'];
+
+        return $Block;
+    }
+
+    protected function buildFootnoteElement()
+    {
+        $Element = array(
+            'name' => 'div',
+            'attributes' => array('class' => 'footnotes'),
+            'handler' => 'elements',
+            'text' => array(
+                array(
+                    'name' => 'hr',
+                ),
+                array(
+                    'name' => 'ol',
+                    'handler' => 'elements',
+                    'text' => array(),
+                ),
+            ),
+        );
+
+        uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
+
+        foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
+        {
+            if ( ! isset($DefinitionData['number']))
+            {
+                continue;
+            }
+
+            $text = $DefinitionData['text'];
+
+            $text = parent::text($text);
+
+            $numbers = range(1, $DefinitionData['count']);
+
+            $backLinksMarkup = '';
+
+            foreach ($numbers as $number)
+            {
+                $backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">&#8617;</a>';
+            }
+
+            $backLinksMarkup = substr($backLinksMarkup, 1);
+
+            if (substr($text, - 4) === '</p>')
+            {
+                $backLinksMarkup = '&#160;'.$backLinksMarkup;
+
+                $text = substr_replace($text, $backLinksMarkup.'</p>', - 4);
+            }
+            else
+            {
+                $text .= "\n".'<p>'.$backLinksMarkup.'</p>';
+            }
+
+            $Element['text'][1]['text'] []= array(
+                'name' => 'li',
+                'attributes' => array('id' => 'fn:'.$definitionId),
+                'text' => "\n".$text."\n",
+            );
+        }
+
+        return $Element;
+    }
+
+    # ~
+
+    protected function parseAttributeData($attributeString)
+    {
+        $Data = array();
+
+        $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
+
+        foreach ($attributes as $attribute)
+        {
+            if ($attribute[0] === '#')
+            {
+                $Data['id'] = substr($attribute, 1);
+            }
+            else # "."
+            {
+                $classes []= substr($attribute, 1);
+            }
+        }
+
+        if (isset($classes))
+        {
+            $Data['class'] = implode(' ', $classes);
+        }
+
+        return $Data;
+    }
+
+    # ~
+
+    protected function processTag($elementMarkup) # recursive
+    {
+        # http://stackoverflow.com/q/1148928/200145
+        libxml_use_internal_errors(true);
+
+        $DOMDocument = new \DOMDocument;
+
+        # http://stackoverflow.com/q/11309194/200145
+        $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
+
+        # http://stackoverflow.com/q/4879946/200145
+        $DOMDocument->loadHTML($elementMarkup);
+        $DOMDocument->removeChild($DOMDocument->doctype);
+        $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
+
+        $elementText = '';
+
+        if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
+        {
+            foreach ($DOMDocument->documentElement->childNodes as $Node)
+            {
+                $elementText .= $DOMDocument->saveHTML($Node);
+            }
+
+            $DOMDocument->documentElement->removeAttribute('markdown');
+
+            $elementText = "\n".$this->text($elementText)."\n";
+        }
+        else
+        {
+            foreach ($DOMDocument->documentElement->childNodes as $Node)
+            {
+                $nodeMarkup = $DOMDocument->saveHTML($Node);
+
+                if ($Node instanceof \DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
+                {
+                    $elementText .= $this->processTag($nodeMarkup);
+                }
+                else
+                {
+                    $elementText .= $nodeMarkup;
+                }
+            }
+        }
+
+        # because we don't want for markup to get encoded
+        $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
+
+        $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
+        $markup = str_replace('placeholder\x1A', $elementText, $markup);
+
+        return $markup;
+    }
+
+    # ~
+
+    protected function sortFootnotes($A, $B) # callback
+    {
+        return $A['number'] - $B['number'];
+    }
+
+    #
+    # Fields
+    #
+
+    protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
+}

+ 3 - 4
system/src/Grav/Framework/Route/Route.php

@@ -286,7 +286,7 @@ class Route
             $url .= '?' . $this->getUriQuery();
         }
 
-        return $url;
+        return rtrim($url,'/');
     }
 
     /**
@@ -344,9 +344,8 @@ class Route
             $parts[] = $this->language;
         }
 
-        if ($this->route !== '') {
-            $parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route;
-        }
+        $parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route;
+
 
         if ($this->gravParams) {
             $parts[] = RouteFactory::buildParams($this->gravParams);

+ 30 - 24
system/src/Grav/Framework/Session/Session.php

@@ -9,6 +9,7 @@
 
 namespace Grav\Framework\Session;
 
+use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Framework\Session\Exceptions\SessionException;
 
 /**
@@ -17,16 +18,13 @@ use Grav\Framework\Session\Exceptions\SessionException;
  */
 class Session implements SessionInterface
 {
-    protected $options;
+    /** @var array */
+    protected $options = [];
 
-    /**
-     * @var bool
-     */
+    /** @var bool */
     protected $started = false;
 
-    /**
-     * @var Session
-     */
+    /** @var Session */
     protected static $instance;
 
     /**
@@ -178,9 +176,13 @@ class Session implements SessionInterface
             return $this;
         }
 
+        $sessionName = session_name();
+        $sessionExists = isset($_COOKIE[$sessionName]);
+
         // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836
-        if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) {
-            unset($_COOKIE[session_name()]);
+        if ($sessionExists && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[$sessionName])) {
+            unset($_COOKIE[$sessionName]);
+            $sessionExists = false;
         }
 
         $options = $this->options;
@@ -197,24 +199,28 @@ class Session implements SessionInterface
             throw new SessionException('Failed to start session: ' . $error, 500);
         }
 
-        if ($user && !$user->isValid()) {
-            $this->clear();
-            throw new SessionException('User Invalid', 500);
-        }
+        $this->started = true;
 
-        $params = session_get_cookie_params();
+        if ($user && (!$user instanceof UserInterface || !$user->isValid())) {
+            $this->invalidate();
 
-        setcookie(
-            session_name(),
-            session_id(),
-            time() + $params['lifetime'],
-            $params['path'],
-            $params['domain'],
-            $params['secure'],
-            $params['httponly']
-        );
+            throw new SessionException('Invalid User object, session destroyed.', 500);
+        }
 
-        $this->started = true;
+        // Extend the lifetime of the session.
+        if ($sessionExists) {
+            $params = session_get_cookie_params();
+
+            setcookie(
+                $sessionName,
+                session_id(),
+                time() + $params['lifetime'],
+                $params['path'],
+                $params['domain'],
+                $params['secure'],
+                $params['httponly']
+            );
+        }
 
         return $this;
     }

+ 1 - 0
user/plugins/admin/.gitignore

@@ -1,5 +1,6 @@
 themes/grav/.sass-cache
 .DS_Store
+crowdin.yaml
 
 # Node Modules
 **/node_modules/**

+ 65 - 15
user/plugins/admin/CHANGELOG.md

@@ -1,3 +1,53 @@
+# v1.9.16
+## 09/01/2020
+
+1. [](#bugfix)
+    * Fixed a glitch which allows user to delete entire pages directory [#1941](https://github.com/getgrav/grav-plugin-admin/issues/1941)
+    * Fixed the hidden login plugin toggle
+
+# v1.9.15
+## 06/08/2020
+
+1. [](#bugfix)
+    * Support markdown in `fieldset.text` [#2934](https://github.com/getgrav/grav/issues/2934)
+    * Fix data URLs in avatar images [#1889](https://github.com/getgrav/grav/issues/1889)
+    * Fix for deleting files in plugin configurations
+
+# v1.9.14
+## 04/27/2020
+
+1. [](#improved)
+    * Added `slug` and `type` to blueprints
+1. [](#bugfix)
+    * Support markdown in `fieldset.text` [#2934](https://github.com/getgrav/grav/issues/2934)
+
+# v1.9.13
+## 03/05/2020
+
+1. [](#improved)
+    * Updated vendor libs
+1. [](#bugfix)
+    * Fixed toggleable buttons no longer holding false state [form#406](ttps://github.com/getgrav/grav-plugin-form/issues/406)
+
+# v1.9.12
+## 12/04/2019
+
+1. [](#bugfix)
+    * Fixed saving configuration in PHP 7.4
+
+# v1.9.11
+## 11/06/2019
+
+1. [](#improved)
+    * Added new "secure delete" functionality [#1752](https://github.com/getgrav/grav-plugin-admin/issues/1752)
+    * Center text logo [#1751](https://github.com/getgrav/grav-plugin-admin/issues/1751)
+    * Added required span to editor field [#1748](https://github.com/getgrav/grav-plugin-admin/issues/1748)
+    * Warn users if JS is disabled [#1722](https://github.com/getgrav/grav-plugin-admin/issues/1722)
+    * Added target rule to quick links [#1518](https://github.com/getgrav/grav-plugin-admin/issues/1518)
+1. [](#bugfix)
+    * Fixed `Badly encoded JSON data` warning when uploading files [grav#2663](https://github.com/getgrav/grav/issues/2663)
+    * Fixed `accept` for SVG in `file` uploaders [#1732](https://github.com/getgrav/grav-plugin-admin/issues/1732)
+
 # v1.9.10
 ## 09/19/2019
 
@@ -24,13 +74,13 @@
     * Fixed issue with `array` field nested in `list` that were losing their index order when the list reordered
     * Fixed file form field failing resolution checks in certain circumstances
     * Fixed issue with deleting files in config based YAML files
-    
+
 # v1.9.7
 ## 06/21/2019
 
 1. [](#bugfix)
     * Fixed issue with charts in dashboard where label would cut off [#1700](https://github.com/getgrav/grav-plugin-admin/issues/1700)
-    * Resetting a user's password clears the user's site access [grav#2528](https://github.com/getgrav/grav/issues/2528) 
+    * Resetting a user's password clears the user's site access [grav#2528](https://github.com/getgrav/grav/issues/2528)
     * Fixed issue with permissions toggle [#1702](https://github.com/getgrav/grav-plugin-admin/issues/1702)
 
 # v1.9.6
@@ -72,7 +122,7 @@
     * Added a new **YAML Linter** report to the `Tools - Reports` section
 1. [](#improved)
     * Updated package.json scripts to properly use gulp compiler
-    
+
 # v1.9.2
 ## 04/15/2019
 
@@ -85,7 +135,7 @@
 
 1. [](#bugfix)
     * Fix for Page saving issues [#1648](https://github.com/getgrav/grav-plugin-admin/issues/1648)
-    * Remove status message when picking folder for move [#1650](https://github.com/getgrav/grav-plugin-admin/issues/1650) 
+    * Remove status message when picking folder for move [#1650](https://github.com/getgrav/grav-plugin-admin/issues/1650)
 
 # v1.9.0
 ## 04/11/2019
@@ -120,10 +170,10 @@
     * Homepage link should be `https://` [#1564](https://github.com/getgrav/grav-plugin-admin/issues/1564)
     * Improve lang string to describe XSS security settings [#1566](https://github.com/getgrav/grav-plugin-admin/issues/1566)
     * Take admin setting for 2FA into account when showing user 2FA badge [#1568](https://github.com/getgrav/grav-plugin-admin/issues/1568)
-    * Moved `ignore` and `key` field into form plugin  
+    * Moved `ignore` and `key` field into form plugin
     * Improved usability of `System` configuration blueprint with side-tabs
-    * Cleaned up UI in `Scheduler` tools page 
-    * Updated languages     
+    * Cleaned up UI in `Scheduler` tools page
+    * Updated languages
 1. [](#bugfix)
     * Fixed user edit links if Flex Objects plugin is installed but user isn't Flex User
     * Fixed deprecated `sameas()` Twig test
@@ -133,15 +183,15 @@
     * Fixed issue with Safari browser and blueprint fields with `toggleable: true` [#1643](https://github.com/getgrav/grav-plugin-admin/issues/1643)
     * Incorrect 2FA lang code [#1618](https://github.com/getgrav/grav-plugin-admin/issues/1618)
     * Fixed potential undefined property in `onPageNotFound` event handling
-    * Proper fix for `vUndefined` when updating plugins/themes 
+    * Proper fix for `vUndefined` when updating plugins/themes
     * Text in Tab Tools/Direct install disappears [#1613](https://github.com/getgrav/grav-plugin-admin/issues/1613)
     * Fallback to page `slug` in Pages list if title is empty [grav#2267](https://github.com/getgrav/grav/issues/2267)
     * Fixes backup button issues with `;` param separator [#1602](https://github.com/getgrav/grav-plugin-admin/issues/1602) [#1502](https://github.com/getgrav/grav-plugin-admin/issues/1502)
     * Set default state for `show_modular` to `true` [#1599](https://github.com/getgrav/grav-plugin-admin/issues/1599)
     * Removed `tabs`, `tab`, and `toggle` fields as they are now in Form plugin
     * Fix issue with new page always showing modular page templates [#1573](https://github.com/getgrav/grav-plugin-admin/issues/1573)
-    * Fixed issue deleting files in plugins/themes/config 
-    * Fixed array support in admin languages, e.g. `DAYS_OF_THE_WEEK`    
+    * Fixed issue deleting files in plugins/themes/config
+    * Fixed array support in admin languages, e.g. `DAYS_OF_THE_WEEK`
     * Fixed user login / remember me triggering before admin gets initialized
     * Fixed a bug when deleting files via AJAX
     * Fixed error page not to be the frontend version
@@ -154,24 +204,24 @@
     * Removed duplicate language strings
     * Fixed default `job_at` so it does not fail if missing
     * Minor JS group `bottom` fix
- 
+
 # v1.8.20
 ## 03/20/2019
 
 1. [](#improved)
     * Added security field to column [#1622](https://github.com/getgrav/grav-plugin-admin/pull/1622)
-    
+
 # v1.8.19
 ## 02/13/2019
 
 1. [](#bugfix)
-    * Moved `show_modular` to proper place - Doh! [grav#2362](https://github.com/getgrav/grav/issues/2362)  
+    * Moved `show_modular` to proper place - Doh! [grav#2362](https://github.com/getgrav/grav/issues/2362)
 
 # v1.8.18
 ## 02/12/2019
 
 1. [](#bugfix)
-    * Set default value for `show_modular` [grav#2362](https://github.com/getgrav/grav/issues/2362)  
+    * Set default value for `show_modular` [grav#2362](https://github.com/getgrav/grav/issues/2362)
 
 # v1.8.17
 ## 02/07/2019
@@ -192,7 +242,7 @@
 1. [](#bugfix)
     * Fixed calendar js module not properly loading for datetime field [#1581](https://github.com/getgrav/grav-plugin-admin/issues/1581)
     * Fixed deleting file when using file field type [#1558](https://github.com/getgrav/grav-plugin-admin/issues/1558)
-    * Unset state from user if not super or user admin   
+    * Unset state from user if not super or user admin
 
 # v1.8.15
 ## 12/14/2018

+ 1 - 0
user/plugins/admin/admin.yaml

@@ -26,6 +26,7 @@ session:
   timeout: 1800
 warnings:
   delete_page: true
+  secure_delete: false
 edit_mode: normal
 frontend_preview_target: inline
 show_github_msg: true

+ 16 - 2
user/plugins/admin/blueprints.yaml

@@ -1,5 +1,7 @@
 name: Admin Panel
-version: 1.9.10
+slug: admin
+type: plugin
+version: 1.9.16
 testing: false
 description: Adds an advanced administration panel to manage your site
 icon: empire
@@ -14,7 +16,7 @@ docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
 license: MIT
 
 dependencies:
-    - { name: grav, version: '>=1.6.8' }
+    - { name: grav, version: '>=1.6.22' }
     - { name: form, version: '>=3.0.0' }
     - { name: login, version: '>=3.0.0' }
     - { name: email, version: '>=3.0.0' }
@@ -243,6 +245,18 @@ form:
         type: bool
       help: Ask the user confirmation when deleting a page
 
+    warnings.secure_delete:
+      type: toggle
+      label: Secure Delete
+      highlight: 1
+      default: 1
+      options:
+        1: PLUGIN_ADMIN.ENABLED
+        0: PLUGIN_ADMIN.DISABLED
+      validate:
+        type: bool
+      help: Shows the user a field to enter the word DELETE and enable the confirm delete button.
+
     hide_page_types:
       type: array
       label: Hide page types in Admin

+ 4 - 1
user/plugins/admin/classes/admin.php

@@ -1671,8 +1671,11 @@ class Admin
     {
         $reports = new ArrayCollection();
 
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+
         // Default to XSS Security Report
-        $result = Security::detectXssFromPages($this->grav['pages'], true);
+        $result = Security::detectXssFromPages($pages, true);
 
         $reports['Grav Security Check'] = $this->grav['twig']->processTemplate('reports/security.html.twig', [
             'result' => $result,

+ 3 - 3
user/plugins/admin/classes/adminbasecontroller.php

@@ -325,7 +325,7 @@ class AdminBaseController
             }
 
             $isMime = strstr($type, '/');
-            $find   = str_replace(['.', '*'], ['\.', '.*'], $type);
+            $find   = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
 
             if ($isMime) {
                 $match = preg_match('#' . $find . '$#', $mime);
@@ -737,8 +737,8 @@ class AdminBaseController
         // Process previously uploaded files for the current URI
         // and finally store them. Everything else will get discarded
         $queue = $this->admin->session()->getFlashObject('files-upload');
-        $queue = $queue[base64_encode($this->grav['uri']->url())];
         if (is_array($queue)) {
+            $queue = $queue[base64_encode($this->grav['uri']->url())];
             foreach ($queue as $key => $files) {
                 foreach ($files as $destination => $file) {
                     if (!rename($file['tmp_name'], $destination)) {
@@ -943,7 +943,7 @@ class AdminBaseController
             $settings = (object)$blueprints->schema()->getProperty($field);
         } else {
             $page = null;
-            if ($type === 'themes') {
+            if ($type === 'themes' || $type === 'plugins') {
                 $obj = $this->grav[$type]->get(Utils::substrToString($blueprint, '/')); //here
                 $settings = (object) $obj->blueprints()->schema()->getProperty($field);
             } else {

+ 15 - 1
user/plugins/admin/classes/admincontroller.php

@@ -487,6 +487,11 @@ class AdminController extends AdminBaseController
 
         $data = (array)$this->data;
 
+        $folder = $data['folder'] ?? '';
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Creating folder failed, bad folder name', 400);
+        }
+
         if ($data['route'] === '/') {
             $path = $this->grav['locator']->findResource('page://');
         } else {
@@ -494,7 +499,7 @@ class AdminController extends AdminBaseController
         }
 
         $orderOfNewFolder = static::getNextOrderInFolder($path);
-        $new_path         = $path . '/' . $orderOfNewFolder . '.' . $data['folder'];
+        $new_path         = $path . '/' . $orderOfNewFolder . '.' . $folder;
 
         Folder::create($new_path);
         Cache::clearCache('invalidate');
@@ -585,6 +590,11 @@ class AdminController extends AdminBaseController
         /** @var PageInterface $obj */
         $obj = $this->admin->page(true);
 
+        $folder = $data['folder'] ?? null;
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Saving page failed: bad folder name', 400);
+        }
+
         if (!isset($data['folder']) || !$data['folder']) {
             $data['folder'] = $obj->slug();
             $this->data['folder'] = $obj->slug();
@@ -814,6 +824,10 @@ class AdminController extends AdminBaseController
             $folder = \Grav\Plugin\Admin\Utils::slug($data[substr($folder, 9)]);
         }
         $folder = ltrim($folder, '_');
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Creating page failed: bad folder name', 400);
+        }
+
         if (!empty($data['modular'])) {
             $folder = '_' . $folder;
         }

File diff suppressed because it is too large
+ 242 - 180
user/plugins/admin/composer.lock


+ 6 - 0
user/plugins/admin/languages/en.yaml

@@ -1,4 +1,5 @@
 PLUGIN_ADMIN:
+  ADMIN_NOSCRIPT_MSG: "Please enable JavaScript in your browser."
   ADMIN_BETA_MSG: "This is a Beta release! Use this in production at your own risk..."
   ADMIN_REPORT_ISSUE: "Found an issue? Please report it on GitHub."
   EMAIL_FOOTER: "<a href=\"http://getgrav.org\">Powered by Grav</a> - The Modern Flat File CMS"
@@ -43,10 +44,12 @@ PLUGIN_ADMIN:
   EXPERT: "Expert"
   EXPAND_ALL: "Expand All"
   COLLAPSE_ALL: "Collapse All"
+  SECURE_DELETE_DESC: "Type DELETE and confirm"
   ERROR: "Error"
   CLOSE: "Close"
   CANCEL: "Cancel"
   CONTINUE: "Continue"
+  CONFIRM: "Confirm"
   MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Confirmation Required"
   MODAL_CHANGED_DETECTED_TITLE: "Changes Detected"
   MODAL_CHANGED_DETECTED_DESC: "You have unsaved changes.  Are you sure you want to leave without saving?"
@@ -825,3 +828,6 @@ PLUGIN_ADMIN:
   DEBUGGER_CLOCKWORK: "Clockwork Browser Extension"
   SHOW_SENSITIVE: "Show Sensitive Data"
   SHOW_SENSITIVE_HELP: "Clockwork Provider ONLY: Censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)"
+  VALID_LINK_ATTRIBUTES: "Valid Link Attributes"
+  VALID_LINK_ATTRIBUTES_HELP: "Attributes that will be automatically added to the media HTML element"
+

+ 2 - 0
user/plugins/admin/languages/pt.yaml

@@ -43,10 +43,12 @@ PLUGIN_ADMIN:
   EXPERT: "Esperto"
   EXPAND_ALL: "Expandir Tudo"
   COLLAPSE_ALL: "Recolher Tudo"
+  SECURE_DELETE_DESC: "Digite DELETE e confirme"
   ERROR: "Erro"
   CLOSE: "Fechar"
   CANCEL: "Cancelar"
   CONTINUE: "Continuar"
+  CONFIRM: "Confirmar"
   MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Confirmação Requerida"
   MODAL_CHANGED_DETECTED_TITLE: "Alterações Detectadas"
   MODAL_CHANGED_DETECTED_DESC: "Você tem alterações não salvas. Tem certeza que deseja sair sem salvar?"

File diff suppressed because it is too large
+ 0 - 0
user/plugins/admin/themes/grav/css-compiled/preset.css.map


File diff suppressed because it is too large
+ 0 - 0
user/plugins/admin/themes/grav/css-compiled/template.css


File diff suppressed because it is too large
+ 0 - 0
user/plugins/admin/themes/grav/css-compiled/template.css.map


+ 1 - 1
user/plugins/admin/themes/grav/scss/fonts.scss

@@ -3,7 +3,7 @@ $fonts-default: 'Roboto' !default;
 $fonts-mono: 'Inconsolata' !default;
 
 $font-definitions: (
-    Josefin+Slab: 400,
+    'Josefin Slab': '400',
     Roboto: '300,400,500',
     Inconsolata: '400,700'
 );

+ 2 - 1
user/plugins/admin/themes/grav/scss/template/_admin.scss

@@ -244,7 +244,7 @@ $content-padding: 1.5rem;
 
         display: flex;
         align-items: center;
-        
+
         a:not(.front-end) {
             display: inherit;
         }
@@ -798,6 +798,7 @@ body.sidebar-quickopen #admin-main {
 
             .button {
                 margin: 0;
+                height: 37px;
 
                 &.dropdown-toggle {
                     margin-left: -4px;

+ 1 - 0
user/plugins/admin/themes/grav/scss/template/_login.scss

@@ -103,6 +103,7 @@
         justify-content: center;
 
         &.text-logo {
+            text-align: center;
             font-size: 4rem;
         }
 

+ 4 - 1
user/plugins/admin/themes/grav/scss/template/modules/_buttons.scss

@@ -24,7 +24,10 @@
         font-size: 90%;
     }
 
-
+    &:disabled {
+        opacity: .6;
+        cursor: no-drop;
+    }
 
 }
 

+ 2 - 2
user/plugins/admin/themes/grav/templates/forms/field.html.twig

@@ -2,9 +2,9 @@
 
 {% if not blueprints or (blueprints.schema.type(field.type)['input@'] ?? true) is same as(true) %}
     {% set originalValue = originalValue is defined ? originalValue : value %}
-    {% set toggleableChecked = field.toggleable and (originalValue is not null and originalValue is not empty) %}
+    {% set toggleableChecked = field.toggleable and originalValue is not null %}
     {% set isDisabledToggleable = field.toggleable and not toggleableChecked %}
-    {% set value = (value is null ? field.default : value) %}
+    {% set value = value ?? field.default %}
 
     {% if (field.yaml or field.validate.type == 'yaml') and value is iterable%}
         {% set value = value|toYaml %}

+ 4 - 1
user/plugins/admin/themes/grav/templates/forms/fields/editor/editor.html.twig

@@ -11,7 +11,10 @@
             {% if field.help %}
                 {% set hint = 'data-hint="' ~ field.help|tu|raw ~ '"' %}
             {% endif %}
-            <div class="form-label form-field hint--bottom" {{ hint|raw }}>{{ field.label|tu|raw }}</div>
+            <div class="form-label form-field hint--bottom" {{ hint|raw }}>
+                {{ field.label|tu|raw }}
+                {{ field.validate.required in ['on', 'true', 1] ? '<span class="required">*</span>' }}
+            </div>
         {% endif %}
     {% endblock %}
     <div class="form-field {{ field.classes|default('') }}">

+ 6 - 3
user/plugins/admin/themes/grav/templates/forms/fields/fieldset/fieldset.html.twig

@@ -48,9 +48,12 @@
       >
 
           {% block group %}
-          {% if field.text %}
-          <p>{% if grav.twig.twig.filters['tu'] is defined %}{{ field.text|tu }}{% else %}{{ field.text|t }}{% endif %}
-          <p>
+              {% if field.text %}
+                {% if grav.twig.twig.filters['tu'] is defined %}
+                    {{ field.markdown ? field.text|tu|markdown : '<p>' ~ field.text|tu ~ '</p>' }}
+                {% else %}
+                    {{ field.markdown ? field.text|t|markdown : '<p>' ~ field.t ~ '</p>' }}
+                {% endif %}
               {% endif %}
 
               {% if field.fields %}

+ 24 - 1
user/plugins/admin/themes/grav/templates/pages.html.twig

@@ -107,6 +107,7 @@
 
 {% set modular = context.modular ? 'modular_' : '' %}
 {% set warn = config.plugins.admin.warnings.delete_page %}
+{% set secure_delete = config.plugins.admin.warnings.secure_delete %}
 {% set admin_lang = admin.session.admin_lang ?: 'en' %}
 {% set page_lang = context.language %}
 {% set type = 'page' %}
@@ -405,10 +406,19 @@
             <p class="bigger">
               {{ "PLUGIN_ADMIN.MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC"|tu }}
             </p>
+            {% if secure_delete %}
+                <p class="form-secure-delete">
+                    <input id="secure-delete-field" autofocus type="text" placeholder="{{ "PLUGIN_ADMIN.SECURE_DELETE_DESC"|tu }}" />
+                </p>
+            {% endif %}
             <br>
             <div class="button-bar">
             <button data-remodal-action="cancel" class="button secondary remodal-cancel"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</button>
-            <a class="button disable-after-click" data-delete-action href="#"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</a>
+            {% if secure_delete %}
+                <button id="secure-delete-btn" disabled="true" class="button danger disable-after-click" data-delete-action><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONFIRM"|tu }}</button>
+            {% else %}
+                <a class="button danger disable-after-click" data-delete-action href="#"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</a>
+            {% endif %}
             </div>
         </form>
     </div>
@@ -421,5 +431,18 @@
     {{ parent() }}
     <script>
         $('.admin-pages .form-tabs .tabs-nav').css('margin-right', ($('#admin-topbar').width() + 20) + 'px');
+        {% if secure_delete %}
+        $('#secure-delete-field').keyup(function () {
+            var inputValue = $(this).val();
+            if (inputValue == 'DELETE') {
+                $('#secure-delete-btn').attr('disabled', false);
+            }else{
+                $('#secure-delete-btn').attr('disabled', true);
+            }
+        });
+        $(document).on('closing', '.remodal', function () {
+            $('#secure-delete-field').val('');
+        });
+        {% endif %}
     </script>
 {% endblock %}

+ 4 - 0
user/plugins/admin/themes/grav/templates/partials/base-root.html.twig

@@ -40,6 +40,10 @@
         {% include 'partials/messages.html.twig' %}
     {% else %}
 
+        {% block noscript %}
+            {% include 'partials/noscript.html.twig' %}
+        {% endblock noscript %}
+
         {% block page %}
         <div class="remodal-bg">
 

+ 1 - 1
user/plugins/admin/themes/grav/templates/partials/nav-quick-tray.html.twig

@@ -18,7 +18,7 @@
             {% endif %}
             <li class="{{ item.class }} hint--bottom" data-hint="{{ item.hint }}" {{ data_tags|raw }}>
             {% if item.route %}
-                <a href="{{ url(item.route) }}">
+                <a href="{{ url(item.route) }}" {% if item.target %}target="{{ item.target }}"{% endif %}>
                     <i class="fa fa-fw {{ item.icon }}"></i>
                 </a>
             {% else %}

+ 1 - 1
user/plugins/admin/themes/grav/templates/partials/nav-user-avatar.html.twig

@@ -1,2 +1,2 @@
 {% set user_avatar = admin.user.getAvatarUrl() %}
-<img src="{{ '?' not in user_avatar ? user_avatar ~ '?s=80' : user_avatar }}" />
+<img src="{{ ('?' not in user_avatar) and (not user_avatar starts with 'data:') ? user_avatar ~ '?s=80' : user_avatar }}" />

+ 42 - 0
user/plugins/admin/themes/grav/templates/partials/noscript.html.twig

@@ -0,0 +1,42 @@
+<style>
+.error-message {
+  background-color: #fce4e4;
+  border: 1px solid #fcc2c3;
+  width: 100%;
+  padding: 20px 30px;
+}
+.error-text {
+  color: #cc0033;
+  font-family: Helvetica, Arial, sans-serif;
+  width: 100%;
+  font-weight: bold;
+  line-height: 20px;
+  text-shadow: 1px 1px rgba(250,250,250,.3);
+}
+.full-height {
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+</style>
+
+<div id="noscript" class="full-height">
+    <main id="admin-main" class="full-height">
+        <div id="titlebar" class="titlebar">
+            <h1><i class="fa fa-fw fa-exclamation-triangle"></i>{{ "PLUGIN_ADMIN.ERROR"|tu }}</h1>
+        </div>
+        <div class="full-height">
+                <div class="error-message {% if config.plugins.admin.content_padding %}content-padding{% endif %}">
+                    <span class="error-text">{{ "PLUGIN_ADMIN.ADMIN_NOSCRIPT_MSG"|tu }}</span>
+                </div>
+        </div>
+    </main>
+</div>
+
+<script type="text/javascript">
+  function checkjs() {
+    var element = document.getElementById("noscript");
+    element.style.display = 'none';
+  }
+  checkjs();
+</script>

+ 1 - 1
user/plugins/admin/themes/grav/templates/partials/plugins-list.html.twig

@@ -42,7 +42,7 @@
                 {% if isTestingRelease %}<span class="gpm-testing">test release</span>{% endif %}
             </td>
             <td class="gpm-actions">
-                {% if (not installing and (plugin.form.fields.enabled.type != 'hidden' and plugin.form.fields.tabs.fields.login.fields.enabled.type != 'hidden')) %}
+                {% if (not installing and (plugin.form.fields.enabled.type != 'hidden' and plugin.form.fields.tabs.fields.options.fields.enabled.type != 'hidden')) %}
                     <a class="{{ data.get('enabled') ? 'enabled' : 'disabled' }}" href="{{ uri.addNonce(base_url_relative ~ '/plugins/' ~ slug ~ '/task' ~ config.system.param_sep ~ (data.get('enabled') ? 'disable' : 'enable'), 'admin-form', 'admin-nonce') }}">
                         <i class="fa fa-fw fa-toggle-{{ data.get('enabled') ? 'on' : 'off' }}"></i>
                     </a>

+ 2 - 2
user/plugins/admin/vendor/composer/ClassLoader.php

@@ -279,7 +279,7 @@ class ClassLoader
      */
     public function setApcuPrefix($apcuPrefix)
     {
-        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
     }
 
     /**
@@ -377,7 +377,7 @@ class ClassLoader
             $subPath = $class;
             while (false !== $lastPos = strrpos($subPath, '\\')) {
                 $subPath = substr($subPath, 0, $lastPos);
-                $search = $subPath.'\\';
+                $search = $subPath . '\\';
                 if (isset($this->prefixDirsPsr4[$search])) {
                     $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                     foreach ($this->prefixDirsPsr4[$search] as $dir) {

+ 9 - 9
user/plugins/admin/vendor/composer/installed.json

@@ -1,27 +1,26 @@
 [
     {
         "name": "composer/semver",
-        "version": "1.4.2",
-        "version_normalized": "1.4.2.0",
+        "version": "1.5.1",
+        "version_normalized": "1.5.1.0",
         "source": {
             "type": "git",
             "url": "https://github.com/composer/semver.git",
-            "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+            "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
-            "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+            "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+            "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
             "shasum": ""
         },
         "require": {
             "php": "^5.3.2 || ^7.0"
         },
         "require-dev": {
-            "phpunit/phpunit": "^4.5 || ^5.0.5",
-            "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+            "phpunit/phpunit": "^4.5 || ^5.0.5"
         },
-        "time": "2016-08-30T16:08:34+00:00",
+        "time": "2020-01-13T12:06:48+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
@@ -164,6 +163,7 @@
             "security",
             "xml",
             "zf"
-        ]
+        ],
+        "abandoned": "laminas/laminas-xml"
     }
 ]

+ 12 - 0
user/plugins/admin/vendor/composer/semver/CHANGELOG.md

@@ -3,6 +3,15 @@
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+### [1.5.1] 2020-01-13
+
+  * Fixed: Parsing of aliased version was not validating the alias to be a valid version
+
+### [1.5.0] 2019-03-19
+
+  * Added: some support for date versions (e.g. 201903) in `~` operator
+  * Fixed: support for stabilities in `~` operator was inconsistent
+
 ### [1.4.2] 2016-08-30
 
   * Fixed: collapsing of complex constraints lead to buggy constraints
@@ -57,6 +66,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
     - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint`
   * Changed: code style using php-cs-fixer.
 
+[1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1
+[1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0
+[1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2
 [1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1
 [1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0
 [1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0

+ 1 - 2
user/plugins/admin/vendor/composer/semver/composer.json

@@ -34,8 +34,7 @@
         "php": "^5.3.2 || ^7.0"
     },
     "require-dev": {
-        "phpunit/phpunit": "^4.5 || ^5.0.5",
-        "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+        "phpunit/phpunit": "^4.5 || ^5.0.5"
     },
     "autoload": {
         "psr-4": {

+ 1 - 1
user/plugins/admin/vendor/composer/semver/src/Constraint/AbstractConstraint.php

@@ -11,7 +11,7 @@
 
 namespace Composer\Semver\Constraint;
 
-trigger_error('The ' . __CLASS__ . ' abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED);
+trigger_error('The ' . __NAMESPACE__ . '\AbstractConstraint abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED);
 
 /**
  * Base constraint class.

+ 3 - 7
user/plugins/admin/vendor/composer/semver/src/Constraint/Constraint.php

@@ -184,7 +184,7 @@ class Constraint implements ConstraintInterface
         // '!=' operator is match when other operator is not '==' operator or version is not match
         // these kinds of comparisons always have a solution
         if ($isNonEqualOp || $isProviderNonEqualOp) {
-            return !$isEqualOp && !$isProviderEqualOp
+            return (!$isEqualOp && !$isProviderEqualOp)
                 || $this->versionCompare($provider->version, $this->version, '!=', $compareBranches);
         }
 
@@ -197,13 +197,9 @@ class Constraint implements ConstraintInterface
         if ($this->versionCompare($provider->version, $this->version, self::$transOpInt[$this->operator], $compareBranches)) {
             // special case, e.g. require >= 1.0 and provide < 1.0
             // 1.0 >= 1.0 but 1.0 is outside of the provided interval
-            if ($provider->version === $this->version
+            return !($provider->version === $this->version
                 && self::$transOpInt[$provider->operator] === $providerNoEqualOp
-                && self::$transOpInt[$this->operator] !== $noEqualOp) {
-                return false;
-            }
-
-            return true;
+                && self::$transOpInt[$this->operator] !== $noEqualOp);
         }
 
         return false;

+ 1 - 1
user/plugins/admin/vendor/composer/semver/src/Constraint/EmptyConstraint.php

@@ -46,7 +46,7 @@ class EmptyConstraint implements ConstraintInterface
             return $this->prettyString;
         }
 
-        return $this->__toString();
+        return (string) $this;
     }
 
     /**

+ 1 - 1
user/plugins/admin/vendor/composer/semver/src/Constraint/MultiConstraint.php

@@ -102,7 +102,7 @@ class MultiConstraint implements ConstraintInterface
             return $this->prettyString;
         }
 
-        return $this->__toString();
+        return (string) $this;
     }
 
     /**

+ 2 - 2
user/plugins/admin/vendor/composer/semver/src/Semver.php

@@ -37,9 +37,9 @@ class Semver
 
         $versionParser = self::$versionParser;
         $provider = new Constraint('==', $versionParser->normalize($version));
-        $constraints = $versionParser->parseConstraints($constraints);
+        $parsedConstraints = $versionParser->parseConstraints($constraints);
 
-        return $constraints->matches($provider);
+        return $parsedConstraints->matches($provider);
     }
 
     /**

+ 22 - 25
user/plugins/admin/vendor/composer/semver/src/VersionParser.php

@@ -52,11 +52,12 @@ class VersionParser
     {
         $version = preg_replace('{#.+$}i', '', $version);
 
-        if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) {
+        if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) {
             return 'dev';
         }
 
         preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match);
+
         if (!empty($match[3])) {
             return 'dev';
         }
@@ -107,6 +108,9 @@ class VersionParser
 
         // strip off aliasing
         if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
+            // verify that the alias is a version without constraint
+            $this->normalize($match[2]);
+
             $version = $match[1];
         }
 
@@ -116,7 +120,7 @@ class VersionParser
         }
 
         // if requirement is branch-like, use full name
-        if ('dev-' === strtolower(substr($version, 0, 4))) {
+        if (stripos($version, 'dev-') === 0) {
             return 'dev-' . substr($version, 4);
         }
 
@@ -236,6 +240,7 @@ class VersionParser
 
         $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
         $orGroups = array();
+
         foreach ($orConstraints as $constraints) {
             $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
             if (count($andConstraints) > 1) {
@@ -268,9 +273,9 @@ class VersionParser
             && 2 === count($orGroups[0]->getConstraints())
             && 2 === count($orGroups[1]->getConstraints())
             && ($a = (string) $orGroups[0])
-            && substr($a, 0, 3) === '[>=' && (false !== ($posA = strpos($a, '<', 4)))
+            && strpos($a, '[>=') === 0 && (false !== ($posA = strpos($a, '<', 4)))
             && ($b = (string) $orGroups[1])
-            && substr($b, 0, 3) === '[>=' && (false !== ($posB = strpos($b, '<', 4)))
+            && strpos($b, '[>=') === 0 && (false !== ($posB = strpos($b, '<', 4)))
             && substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5)
         ) {
             $constraint = new MultiConstraint(array(
@@ -314,7 +319,7 @@ class VersionParser
         // version, to ensure that unstable instances of the current version are allowed. However, if a stability
         // suffix is added to the constraint, then a >= match on the current version is used instead.
         if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
-            if (substr($constraint, 0, 2) === '~>') {
+            if (strpos($constraint, '~>') === 0) {
                 throw new \UnexpectedValueException(
                     'Could not parse version constraint ' . $constraint . ': ' .
                     'Invalid operator "~>", you probably meant to use the "~" operator'
@@ -322,11 +327,11 @@ class VersionParser
             }
 
             // Work out which position in the version we are operating at
-            if (isset($matches[4]) && '' !== $matches[4]) {
+            if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) {
                 $position = 4;
-            } elseif (isset($matches[3]) && '' !== $matches[3]) {
+            } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
                 $position = 3;
-            } elseif (isset($matches[2]) && '' !== $matches[2]) {
+            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
                 $position = 2;
             } else {
                 $position = 1;
@@ -334,19 +339,11 @@ class VersionParser
 
             // Calculate the stability suffix
             $stabilitySuffix = '';
-            if (!empty($matches[5])) {
-                $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : '');
-            }
-
-            if (!empty($matches[7])) {
+            if (empty($matches[5]) && empty($matches[7])) {
                 $stabilitySuffix .= '-dev';
             }
 
-            if (!$stabilitySuffix) {
-                $stabilitySuffix = '-dev';
-            }
-
-            $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix;
+            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
             $lowerBound = new Constraint('>=', $lowVersion);
 
             // For upper bound, we increment the position of one more significance,
@@ -368,9 +365,9 @@ class VersionParser
         // versions 0.X >=0.1.0, and no updates for versions 0.0.X
         if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
             // Work out which position in the version we are operating at
-            if ('0' !== $matches[1] || '' === $matches[2]) {
+            if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) {
                 $position = 1;
-            } elseif ('0' !== $matches[2] || '' === $matches[3]) {
+            } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) {
                 $position = 2;
             } else {
                 $position = 3;
@@ -401,9 +398,9 @@ class VersionParser
         // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
         // A partial version range is treated as an X-Range, so the special character is in fact optional.
         if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) {
-            if (isset($matches[3]) && '' !== $matches[3]) {
+            if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
                 $position = 3;
-            } elseif (isset($matches[2]) && '' !== $matches[2]) {
+            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
                 $position = 2;
             } else {
                 $position = 1;
@@ -462,11 +459,11 @@ class VersionParser
             try {
                 $version = $this->normalize($matches[2]);
 
-                if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') {
+                if (!empty($stabilityModifier) && self::parseStability($version) === 'stable') {
                     $version .= '-' . $stabilityModifier;
                 } elseif ('<' === $matches[1] || '>=' === $matches[1]) {
                     if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
-                        if (substr($matches[2], 0, 4) !== 'dev-') {
+                        if (strpos($matches[2], 'dev-') !== 0) {
                             $version .= '-dev';
                         }
                     }
@@ -511,7 +508,7 @@ class VersionParser
 
                     // Return null on a carry overflow
                     if ($i === 1) {
-                        return;
+                        return null;
                     }
                 }
             }

+ 41 - 0
user/plugins/email/CHANGELOG.md

@@ -1,3 +1,44 @@
+# v3.0.9
+## 06/08/2020
+
+1. [](#improved)
+    * Disable password autocomplete in password field
+    * Don't save empty string in password field [#134](https://github.com/getgrav/grav-plugin-email/issues/134)
+
+# v3.0.8
+## 04/27/2020
+
+1. [](#improved)
+    * Updated vendor library files
+    * Use Grav's Parsedown class
+
+# v3.0.7
+## 03/05/2020
+
+1. [](#improved)
+    * Updated email validator library
+1. [](#bugfix)
+    * Fixed `Invalid resource theme://` on CLI command `test-email` on Grav 1.6.21 and later versions [#128](https://github.com/getgrav/grav-plugin-email/issues/128)
+
+# v3.0.6
+## 02/11/2020
+
+1. [](#improved)
+    * Updated email validator library
+
+# v3.0.5
+## 02/03/2020
+
+1. [](#bugfix)
+    * Fixed a date in changelog (no other changes)
+
+# v3.0.4
+## 01/17/2020
+
+1. [](#improved)
+    * Added ZOHO configuration example
+    * Updated SwiftMailer library for PHP 7.4 support
+
 # v3.0.3
 ## 08/16/2019
 

+ 19 - 0
user/plugins/email/README.md

@@ -159,6 +159,25 @@ mailer:
 
 It's that easy!
 
+#### ZOHO
+
+ZOHO is a popular solution for hosted email due to it's great 'FREE' tier.  It's paid options are also very reasonable and combined with the latest UI updates and outstanding security features, it's a solid email option.
+
+In order to get ZOHO working with Grav, you need to send email via a user account.  You can either use your users' password or generate an **App Password** via your ZOHO account (clicking on your avatar once logged in), then navigating to `Two Factor Authentication -> App Passwords -> Generate`. Just enter a unique app name (i.e. `Grav Website`).
+
+NOTE: The SMTP host required can be found in `Settings -> Mail - > Mail Accounts -> POP/IMAP -> SMTP`.  This will provide the SMTP server for this account (it may not be `imap.zoho.com` depending on what region you are in)
+
+```
+mailer:
+  engine: smtp
+  smtp:
+    server: smtp.zoho.com
+    port: 587
+    encryption: tls
+    user: 'ZOHO_EMAIL_ADDRESS'
+    password: 'ZOHO_EMAIL_PASSWORD'
+```
+
 #### Sendmail
 
 Although not as reliable as SMTP not providing as much debug information, sendmail is a simple option as long as your hosting provider is not blocking the default SMTP port `25`:

+ 6 - 3
user/plugins/email/blueprints.yaml

@@ -1,5 +1,7 @@
 name: Email
-version: 3.0.3
+slug: email
+type: plugin
+version: 3.0.9
 testing: false
 description: Enables the emailing system for Grav
 icon: envelope
@@ -18,6 +20,7 @@ dependencies:
 
 form:
   validation: loose
+
   fields:
     enabled:
       type: hidden
@@ -168,13 +171,13 @@ form:
     mailer.smtp.user:
       type: text
       size: medium
-      autocomplete: nope
+      autocomplete: 'off'
       label: PLUGIN_EMAIL.SMTP_LOGIN_NAME
 
     mailer.smtp.password:
       type: password
       size: medium
-      autocomplete: nope
+      autocomplete: 'new-password'
       label: PLUGIN_EMAIL.SMTP_PASSWORD
 
     sendmail_config:

+ 3 - 2
user/plugins/email/classes/Email.php

@@ -4,6 +4,7 @@ namespace Grav\Plugin\Email;
 use Grav\Common\Config\Config;
 use Grav\Common\Grav;
 use Grav\Common\Language\Language;
+use Grav\Common\Markdown\Parsedown;
 use Grav\Common\Twig\Twig;
 use Grav\Framework\Form\Interfaces\FormInterface;
 use \Monolog\Logger;
@@ -178,7 +179,7 @@ class Email
                         $body = $twig->processString($value, $vars);
 
                         if ($params['process_markdown']) {
-                            $parsedown = new \Parsedown();
+                            $parsedown = new Parsedown();
                             $body = $parsedown->text($body);
                         }
 
@@ -202,7 +203,7 @@ class Email
                             $body = !empty($body_part['body']) ? $twig->processString($body_part['body'], $vars) : null;
 
                             if ($params['process_markdown']) {
-                                $parsedown = new \Parsedown();
+                                $parsedown = new Parsedown();
                                 $body = $parsedown->text($body);
                             }
 

+ 5 - 0
user/plugins/email/cli/ClearQueueFailuresCommand.php

@@ -40,6 +40,11 @@ class ClearQueueFailuresCommand extends ConsoleCommand
      */
     protected function serve()
     {
+        // TODO: remove when requiring Grav 1.7+
+        if (method_exists($this, 'initializeGrav')) {
+            $this->initializeGrav();
+        }
+
         $grav = Grav::instance();
 
         $this->output->writeln('');

+ 5 - 0
user/plugins/email/cli/FlushQueueCommand.php

@@ -40,6 +40,11 @@ class FlushQueueCommand extends ConsoleCommand
      */
     protected function serve()
     {
+        // TODO: remove when requiring Grav 1.7+
+        if (method_exists($this, 'initializeGrav')) {
+            $this->initializeGrav();
+        }
+
         $grav = Grav::instance();
 
         $this->output->writeln('');

+ 5 - 4
user/plugins/email/cli/TestEmailCommand.php

@@ -59,6 +59,11 @@ class TestEmailCommand extends ConsoleCommand
      */
     protected function serve()
     {
+        // TODO: remove when requiring Grav 1.7+
+        if (method_exists($this, 'initializeGrav')) {
+            $this->initializeThemes();
+        }
+
         $grav = Grav::instance();
 
         $this->output->writeln('');
@@ -69,8 +74,6 @@ class TestEmailCommand extends ConsoleCommand
 
         $this->output->writeln('');
 
-        require_once __DIR__ . '/../vendor/autoload.php';
-
         $grav['Email'] = new Email();
 
         $to = $this->input->getOption('to') ?: $grav['config']->get('plugins.email.to');
@@ -86,8 +89,6 @@ class TestEmailCommand extends ConsoleCommand
             $body = $grav['language']->translate(['PLUGIN_EMAIL.TEST_EMAIL_BODY', $configuration]);
         }
 
-        // This is the old way....
-        // $sent = EmailUtils::sendEmail($subject, $body, $email_to);
         $sent = EmailUtils::sendEmail(['subject'=>$subject, 'body'=>$body, 'to'=>$to]);
 
         if ($sent) {

+ 126 - 60
user/plugins/email/composer.lock

@@ -8,30 +8,35 @@
     "packages": [
         {
             "name": "doctrine/lexer",
-            "version": "v1.0.1",
+            "version": "1.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/lexer.git",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+                "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
+                "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": "^7.2"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "phpstan/phpstan": "^0.11.8",
+                "phpunit/phpunit": "^8.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.2.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Doctrine\\Common\\Lexer\\": "lib/"
+                "psr-4": {
+                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -39,48 +44,52 @@
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "roman@code-factory.org"
-                },
                 {
                     "name": "Guilherme Blanco",
                     "email": "guilhermeblanco@gmail.com"
                 },
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
                 {
                     "name": "Johannes Schmitt",
                     "email": "schmittjoh@gmail.com"
                 }
             ],
-            "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
             "keywords": [
+                "annotations",
+                "docblock",
                 "lexer",
-                "parser"
+                "parser",
+                "php"
             ],
-            "time": "2014-09-09T13:34:57+00:00"
+            "time": "2019-10-30T14:39:59+00:00"
         },
         {
             "name": "egulias/email-validator",
-            "version": "2.1.7",
+            "version": "2.1.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+                "reference": "ade6887fd9bd74177769645ab5c474824f8a418a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a",
+                "reference": "ade6887fd9bd74177769645ab5c474824f8a418a",
                 "shasum": ""
             },
             "require": {
                 "doctrine/lexer": "^1.0.1",
-                "php": ">= 5.5"
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.10"
             },
             "require-dev": {
-                "dominicsayers/isemail": "dev-master",
-                "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
+                "dominicsayers/isemail": "^3.0.7",
+                "phpunit/phpunit": "^4.8.36|^7.5.15",
                 "satooshi/php-coveralls": "^1.0.1"
             },
             "suggest": {
@@ -89,7 +98,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.1.x-dev"
                 }
             },
             "autoload": {
@@ -115,20 +124,20 @@
                 "validation",
                 "validator"
             ],
-            "time": "2018-12-04T22:38:24+00:00"
+            "time": "2020-02-13T22:36:52+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
-            "version": "v6.2.0",
+            "version": "v6.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707"
+                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707",
-                "reference": "6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
+                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
                 "shasum": ""
             },
             "require": {
@@ -177,20 +186,20 @@
                 "mail",
                 "mailer"
             ],
-            "time": "2019-03-10T07:52:41+00:00"
+            "time": "2019-11-12T09:31:26+00:00"
         },
         {
             "name": "symfony/polyfill-iconv",
-            "version": "v1.10.0",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-iconv.git",
-                "reference": "97001cfc283484c9691769f51cdf25259037eba2"
+                "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/97001cfc283484c9691769f51cdf25259037eba2",
-                "reference": "97001cfc283484c9691769f51cdf25259037eba2",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/ad6d62792bfbcfc385dd34b424d4fcf9712a32c8",
+                "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8",
                 "shasum": ""
             },
             "require": {
@@ -202,7 +211,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
@@ -236,26 +245,40 @@
                 "portable",
                 "shim"
             ],
-            "time": "2018-09-21T06:26:08+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.10.0",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "89de1d44f2c059b266f22c9cc9124ddc4cd0987a"
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/89de1d44f2c059b266f22c9cc9124ddc4cd0987a",
-                "reference": "89de1d44f2c059b266f22c9cc9124ddc4cd0987a",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3",
                 "symfony/polyfill-mbstring": "^1.3",
-                "symfony/polyfill-php72": "^1.9"
+                "symfony/polyfill-php72": "^1.10"
             },
             "suggest": {
                 "ext-intl": "For best performance"
@@ -263,7 +286,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
@@ -279,13 +302,13 @@
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                },
                 {
                     "name": "Laurent Bassin",
                     "email": "laurent@bassin.info"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
                 }
             ],
             "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
@@ -298,20 +321,34 @@
                 "portable",
                 "shim"
             ],
-            "time": "2018-09-30T16:36:12+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.10.0",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
                 "shasum": ""
             },
             "require": {
@@ -323,7 +360,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
@@ -357,20 +394,34 @@
                 "portable",
                 "shim"
             ],
-            "time": "2018-09-21T13:07:52+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
         },
         {
             "name": "symfony/polyfill-php72",
-            "version": "v1.10.0",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631"
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9050816e2ca34a8e916c3a0ae8b9c2fccf68b631",
-                "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747",
                 "shasum": ""
             },
             "require": {
@@ -379,7 +430,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
@@ -412,7 +463,21 @@
                 "portable",
                 "shim"
             ],
-            "time": "2018-09-21T13:07:52+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
         }
     ],
     "packages-dev": [],
@@ -424,5 +489,6 @@
     "platform": {
         "php": ">=7.1.3"
     },
-    "platform-dev": []
+    "platform-dev": [],
+    "plugin-api-version": "1.1.0"
 }

+ 24 - 0
user/plugins/email/email.php

@@ -1,6 +1,7 @@
 <?php
 namespace Grav\Plugin;
 
+use Grav\Common\Data\Data;
 use Grav\Common\Plugin;
 use Grav\Plugin\Email\Email;
 use RocketTheme\Toolbox\Event\Event;
@@ -22,6 +23,7 @@ class EmailPlugin extends Plugin
             'onFormProcessed'           => ['onFormProcessed', 0],
             'onTwigTemplatePaths'       => ['onTwigTemplatePaths', 0],
             'onSchedulerInitialized'    => ['onSchedulerInitialized', 0],
+            'onAdminSave'               => ['onAdminSave', 0],
         ];
     }
 
@@ -48,6 +50,28 @@ class EmailPlugin extends Plugin
         $twig->twig_paths[] = __DIR__ . '/templates';
     }
 
+    /**
+     * Force compile during save if admin plugin save
+     *
+     * @param Event $event
+     */
+    public function onAdminSave(Event $event)
+    {
+        /** @var Data $obj */
+        $obj = $event['object'];
+
+
+
+        if ($obj instanceof Data && $obj->blueprints()->getFilename() === 'email/blueprints') {
+            $current_pw = $this->grav['config']->get('plugins.email.mailer.smtp.password');
+            $new_pw = $obj->get('mailer.smtp.password');
+            if (!empty($current_pw) && empty($new_pw)) {
+                $obj->set('mailer.smtp.password', $current_pw);
+            }
+
+        }
+    }
+
     /**
      * Send email when processing the form data.
      *

+ 2 - 2
user/plugins/email/vendor/composer/ClassLoader.php

@@ -279,7 +279,7 @@ class ClassLoader
      */
     public function setApcuPrefix($apcuPrefix)
     {
-        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
     }
 
     /**
@@ -377,7 +377,7 @@ class ClassLoader
             $subPath = $class;
             while (false !== $lastPos = strrpos($subPath, '\\')) {
                 $subPath = substr($subPath, 0, $lastPos);
-                $search = $subPath.'\\';
+                $search = $subPath . '\\';
                 if (isset($this->prefixDirsPsr4[$search])) {
                     $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                     foreach ($this->prefixDirsPsr4[$search] as $dir) {

+ 1 - 1
user/plugins/email/vendor/composer/autoload_files.php

@@ -8,7 +8,7 @@ $baseDir = dirname($vendorDir);
 return array(
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
     '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
-    'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
     'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
+    'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
     '2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
 );

+ 0 - 1
user/plugins/email/vendor/composer/autoload_namespaces.php

@@ -6,5 +6,4 @@ $vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);
 
 return array(
-    'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib'),
 );

+ 1 - 0
user/plugins/email/vendor/composer/autoload_psr4.php

@@ -13,4 +13,5 @@ return array(
     'Grav\\Plugin\\Email\\' => array($baseDir . '/classes'),
     'Grav\\Plugin\\Console\\' => array($baseDir . '/cli'),
     'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/EmailValidator'),
+    'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib/Doctrine/Common/Lexer'),
 );

+ 3 - 0
user/plugins/email/vendor/composer/autoload_real.php

@@ -13,6 +13,9 @@ class ComposerAutoloaderInitdec5d78def1384d1f20996ded9d7efbf
         }
     }
 
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
     public static function getLoader()
     {
         if (null !== self::$loader) {

+ 7 - 10
user/plugins/email/vendor/composer/autoload_static.php

@@ -9,8 +9,8 @@ class ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf
     public static $files = array (
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
         '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
-        'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
         'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
+        'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
         '2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php',
     );
 
@@ -31,6 +31,10 @@ class ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf
         array (
             'Egulias\\EmailValidator\\' => 23,
         ),
+        'D' => 
+        array (
+            'Doctrine\\Common\\Lexer\\' => 22,
+        ),
     );
 
     public static $prefixDirsPsr4 = array (
@@ -62,15 +66,9 @@ class ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf
         array (
             0 => __DIR__ . '/..' . '/egulias/email-validator/EmailValidator',
         ),
-    );
-
-    public static $prefixesPsr0 = array (
-        'D' => 
+        'Doctrine\\Common\\Lexer\\' => 
         array (
-            'Doctrine\\Common\\Lexer\\' => 
-            array (
-                0 => __DIR__ . '/..' . '/doctrine/lexer/lib',
-            ),
+            0 => __DIR__ . '/..' . '/doctrine/lexer/lib/Doctrine/Common/Lexer',
         ),
     );
 
@@ -83,7 +81,6 @@ class ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf
         return \Closure::bind(function () use ($loader) {
             $loader->prefixLengthsPsr4 = ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf::$prefixLengthsPsr4;
             $loader->prefixDirsPsr4 = ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf::$prefixesPsr0;
             $loader->classMap = ComposerStaticInitdec5d78def1384d1f20996ded9d7efbf::$classMap;
 
         }, null, ClassLoader::class);

+ 131 - 66
user/plugins/email/vendor/composer/installed.json

@@ -1,33 +1,38 @@
 [
     {
         "name": "doctrine/lexer",
-        "version": "v1.0.1",
-        "version_normalized": "1.0.1.0",
+        "version": "1.2.0",
+        "version_normalized": "1.2.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/doctrine/lexer.git",
-            "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+            "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
-            "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+            "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
+            "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
             "shasum": ""
         },
         "require": {
-            "php": ">=5.3.2"
+            "php": "^7.2"
         },
-        "time": "2014-09-09T13:34:57+00:00",
+        "require-dev": {
+            "doctrine/coding-standard": "^6.0",
+            "phpstan/phpstan": "^0.11.8",
+            "phpunit/phpunit": "^8.2"
+        },
+        "time": "2019-10-30T14:39:59+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.0.x-dev"
+                "dev-master": "1.2.x-dev"
             }
         },
         "installation-source": "dist",
         "autoload": {
-            "psr-0": {
-                "Doctrine\\Common\\Lexer\\": "lib/"
+            "psr-4": {
+                "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
             }
         },
         "notification-url": "https://packagist.org/downloads/",
@@ -35,58 +40,62 @@
             "MIT"
         ],
         "authors": [
-            {
-                "name": "Roman Borschel",
-                "email": "roman@code-factory.org"
-            },
             {
                 "name": "Guilherme Blanco",
                 "email": "guilhermeblanco@gmail.com"
             },
+            {
+                "name": "Roman Borschel",
+                "email": "roman@code-factory.org"
+            },
             {
                 "name": "Johannes Schmitt",
                 "email": "schmittjoh@gmail.com"
             }
         ],
-        "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
-        "homepage": "http://www.doctrine-project.org",
+        "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+        "homepage": "https://www.doctrine-project.org/projects/lexer.html",
         "keywords": [
+            "annotations",
+            "docblock",
             "lexer",
-            "parser"
+            "parser",
+            "php"
         ]
     },
     {
         "name": "egulias/email-validator",
-        "version": "2.1.7",
-        "version_normalized": "2.1.7.0",
+        "version": "2.1.17",
+        "version_normalized": "2.1.17.0",
         "source": {
             "type": "git",
             "url": "https://github.com/egulias/EmailValidator.git",
-            "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+            "reference": "ade6887fd9bd74177769645ab5c474824f8a418a"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
-            "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+            "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a",
+            "reference": "ade6887fd9bd74177769645ab5c474824f8a418a",
             "shasum": ""
         },
         "require": {
             "doctrine/lexer": "^1.0.1",
-            "php": ">= 5.5"
+            "php": ">=5.5",
+            "symfony/polyfill-intl-idn": "^1.10"
         },
         "require-dev": {
-            "dominicsayers/isemail": "dev-master",
-            "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
+            "dominicsayers/isemail": "^3.0.7",
+            "phpunit/phpunit": "^4.8.36|^7.5.15",
             "satooshi/php-coveralls": "^1.0.1"
         },
         "suggest": {
             "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
         },
-        "time": "2018-12-04T22:38:24+00:00",
+        "time": "2020-02-13T22:36:52+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "2.0.x-dev"
+                "dev-master": "2.1.x-dev"
             }
         },
         "installation-source": "dist",
@@ -116,17 +125,17 @@
     },
     {
         "name": "swiftmailer/swiftmailer",
-        "version": "v6.2.0",
-        "version_normalized": "6.2.0.0",
+        "version": "v6.2.3",
+        "version_normalized": "6.2.3.0",
         "source": {
             "type": "git",
             "url": "https://github.com/swiftmailer/swiftmailer.git",
-            "reference": "6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707"
+            "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707",
-            "reference": "6fa3232ff9d3f8237c0fae4b7ff05e1baa4cd707",
+            "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
+            "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
             "shasum": ""
         },
         "require": {
@@ -144,7 +153,7 @@
             "ext-intl": "Needed to support internationalized email addresses",
             "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
         },
-        "time": "2019-03-10T07:52:41+00:00",
+        "time": "2019-11-12T09:31:26+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
@@ -180,17 +189,17 @@
     },
     {
         "name": "symfony/polyfill-iconv",
-        "version": "v1.10.0",
-        "version_normalized": "1.10.0.0",
+        "version": "v1.15.0",
+        "version_normalized": "1.15.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/symfony/polyfill-iconv.git",
-            "reference": "97001cfc283484c9691769f51cdf25259037eba2"
+            "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/97001cfc283484c9691769f51cdf25259037eba2",
-            "reference": "97001cfc283484c9691769f51cdf25259037eba2",
+            "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/ad6d62792bfbcfc385dd34b424d4fcf9712a32c8",
+            "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8",
             "shasum": ""
         },
         "require": {
@@ -199,11 +208,11 @@
         "suggest": {
             "ext-iconv": "For best performance"
         },
-        "time": "2018-09-21T06:26:08+00:00",
+        "time": "2020-03-09T19:04:49+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.9-dev"
+                "dev-master": "1.15-dev"
             }
         },
         "installation-source": "dist",
@@ -237,36 +246,50 @@
             "polyfill",
             "portable",
             "shim"
+        ],
+        "funding": [
+            {
+                "url": "https://symfony.com/sponsor",
+                "type": "custom"
+            },
+            {
+                "url": "https://github.com/fabpot",
+                "type": "github"
+            },
+            {
+                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                "type": "tidelift"
+            }
         ]
     },
     {
         "name": "symfony/polyfill-intl-idn",
-        "version": "v1.10.0",
-        "version_normalized": "1.10.0.0",
+        "version": "v1.15.0",
+        "version_normalized": "1.15.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/symfony/polyfill-intl-idn.git",
-            "reference": "89de1d44f2c059b266f22c9cc9124ddc4cd0987a"
+            "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/89de1d44f2c059b266f22c9cc9124ddc4cd0987a",
-            "reference": "89de1d44f2c059b266f22c9cc9124ddc4cd0987a",
+            "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
+            "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
             "shasum": ""
         },
         "require": {
             "php": ">=5.3.3",
             "symfony/polyfill-mbstring": "^1.3",
-            "symfony/polyfill-php72": "^1.9"
+            "symfony/polyfill-php72": "^1.10"
         },
         "suggest": {
             "ext-intl": "For best performance"
         },
-        "time": "2018-09-30T16:36:12+00:00",
+        "time": "2020-03-09T19:04:49+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.9-dev"
+                "dev-master": "1.15-dev"
             }
         },
         "installation-source": "dist",
@@ -283,13 +306,13 @@
             "MIT"
         ],
         "authors": [
-            {
-                "name": "Symfony Community",
-                "homepage": "https://symfony.com/contributors"
-            },
             {
                 "name": "Laurent Bassin",
                 "email": "laurent@bassin.info"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
             }
         ],
         "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
@@ -301,21 +324,35 @@
             "polyfill",
             "portable",
             "shim"
+        ],
+        "funding": [
+            {
+                "url": "https://symfony.com/sponsor",
+                "type": "custom"
+            },
+            {
+                "url": "https://github.com/fabpot",
+                "type": "github"
+            },
+            {
+                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                "type": "tidelift"
+            }
         ]
     },
     {
         "name": "symfony/polyfill-mbstring",
-        "version": "v1.10.0",
-        "version_normalized": "1.10.0.0",
+        "version": "v1.15.0",
+        "version_normalized": "1.15.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/symfony/polyfill-mbstring.git",
-            "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+            "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
-            "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+            "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+            "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
             "shasum": ""
         },
         "require": {
@@ -324,11 +361,11 @@
         "suggest": {
             "ext-mbstring": "For best performance"
         },
-        "time": "2018-09-21T13:07:52+00:00",
+        "time": "2020-03-09T19:04:49+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.9-dev"
+                "dev-master": "1.15-dev"
             }
         },
         "installation-source": "dist",
@@ -362,31 +399,45 @@
             "polyfill",
             "portable",
             "shim"
+        ],
+        "funding": [
+            {
+                "url": "https://symfony.com/sponsor",
+                "type": "custom"
+            },
+            {
+                "url": "https://github.com/fabpot",
+                "type": "github"
+            },
+            {
+                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                "type": "tidelift"
+            }
         ]
     },
     {
         "name": "symfony/polyfill-php72",
-        "version": "v1.10.0",
-        "version_normalized": "1.10.0.0",
+        "version": "v1.15.0",
+        "version_normalized": "1.15.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/symfony/polyfill-php72.git",
-            "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631"
+            "reference": "37b0976c78b94856543260ce09b460a7bc852747"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9050816e2ca34a8e916c3a0ae8b9c2fccf68b631",
-            "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631",
+            "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
+            "reference": "37b0976c78b94856543260ce09b460a7bc852747",
             "shasum": ""
         },
         "require": {
             "php": ">=5.3.3"
         },
-        "time": "2018-09-21T13:07:52+00:00",
+        "time": "2020-02-27T09:26:54+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.9-dev"
+                "dev-master": "1.15-dev"
             }
         },
         "installation-source": "dist",
@@ -419,6 +470,20 @@
             "polyfill",
             "portable",
             "shim"
+        ],
+        "funding": [
+            {
+                "url": "https://symfony.com/sponsor",
+                "type": "custom"
+            },
+            {
+                "url": "https://github.com/fabpot",
+                "type": "github"
+            },
+            {
+                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                "type": "tidelift"
+            }
         ]
     }
 ]

+ 1 - 1
user/plugins/email/vendor/doctrine/lexer/LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2006-2013 Doctrine Project
+Copyright (c) 2006-2018 Doctrine Project
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the "Software"), to deal in

+ 4 - 0
user/plugins/email/vendor/doctrine/lexer/README.md

@@ -1,5 +1,9 @@
 # Doctrine Lexer
 
+Build Status: [![Build Status](https://travis-ci.org/doctrine/lexer.svg?branch=master)](https://travis-ci.org/doctrine/lexer)
+
 Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.
 
 This lexer is used in Doctrine Annotations and in Doctrine ORM (DQL).
+
+https://www.doctrine-project.org/projects/lexer.html

+ 23 - 6
user/plugins/email/vendor/doctrine/lexer/composer.json

@@ -1,9 +1,15 @@
 {
     "name": "doctrine/lexer",
     "type": "library",
-    "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
-    "keywords": ["lexer", "parser"],
-    "homepage": "http://www.doctrine-project.org",
+    "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+    "keywords": [
+        "php",
+        "parser",
+        "lexer",
+        "annotations",
+        "docblock"
+    ],
+    "homepage": "https://www.doctrine-project.org/projects/lexer.html",
     "license": "MIT",
     "authors": [
         {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
@@ -11,14 +17,25 @@
         {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"}
     ],
     "require": {
-        "php": ">=5.3.2"
+        "php": "^7.2"
+    },
+    "require-dev": {
+        "doctrine/coding-standard": "^6.0",
+        "phpstan/phpstan": "^0.11.8",
+        "phpunit/phpunit": "^8.2"
     },
     "autoload": {
-        "psr-0": { "Doctrine\\Common\\Lexer\\": "lib/" }
+        "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" }
+    },
+    "autoload-dev": {
+        "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine" }
     },
     "extra": {
         "branch-alias": {
-            "dev-master": "1.0.x-dev"
+            "dev-master": "1.2.x-dev"
         }
+    },
+    "config": {
+        "sort-packages": true
     }
 }

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