Kévin Tessier 5 years ago
parent
commit
f9d3d02e74
100 changed files with 2741 additions and 699 deletions
  1. 61 1
      CHANGELOG.md
  2. 1 1
      README.md
  3. 8 8
      composer.json
  4. 220 179
      composer.lock
  5. 2 2
      index.php
  6. 36 0
      system/blueprints/config/system.yaml
  7. 0 2
      system/blueprints/pages/default.yaml
  8. 1 0
      system/blueprints/user/account.yaml
  9. 5 1
      system/config/system.yaml
  10. 2 2
      system/defines.php
  11. 15 11
      system/src/Grav/Common/Config/Setup.php
  12. 10 12
      system/src/Grav/Common/Data/Validation.php
  13. 8 1
      system/src/Grav/Common/Debugger.php
  14. 0 3
      system/src/Grav/Common/File/CompiledFile.php
  15. 8 3
      system/src/Grav/Common/GPM/GPM.php
  16. 1 1
      system/src/Grav/Common/GPM/Licenses.php
  17. 3 7
      system/src/Grav/Common/Grav.php
  18. 4 3
      system/src/Grav/Common/Inflector.php
  19. 9 0
      system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php
  20. 29 0
      system/src/Grav/Common/Media/Interfaces/MediaInterface.php
  21. 9 0
      system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php
  22. 112 0
      system/src/Grav/Common/Media/Traits/MediaTrait.php
  23. 9 0
      system/src/Grav/Common/Page/Interfaces/PageInterface.php
  24. 5 3
      system/src/Grav/Common/Page/Media.php
  25. 20 11
      system/src/Grav/Common/Page/Medium/AbstractMedia.php
  26. 9 2
      system/src/Grav/Common/Page/Medium/ImageMedium.php
  27. 8 3
      system/src/Grav/Common/Page/Medium/Medium.php
  28. 34 0
      system/src/Grav/Common/Page/Medium/VideoMedium.php
  29. 62 52
      system/src/Grav/Common/Page/Page.php
  30. 46 24
      system/src/Grav/Common/Page/Pages.php
  31. 20 9
      system/src/Grav/Common/Processors/InitializeProcessor.php
  32. 13 1
      system/src/Grav/Common/Service/ConfigServiceProvider.php
  33. 17 20
      system/src/Grav/Common/Service/PageServiceProvider.php
  34. 27 20
      system/src/Grav/Common/Service/SessionServiceProvider.php
  35. 17 38
      system/src/Grav/Common/Session.php
  36. 1 1
      system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php
  37. 3 3
      system/src/Grav/Common/Twig/Node/TwigNodeScript.php
  38. 3 3
      system/src/Grav/Common/Twig/Node/TwigNodeStyle.php
  39. 12 10
      system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php
  40. 6 1
      system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php
  41. 1 1
      system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php
  42. 1 1
      system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php
  43. 30 43
      system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php
  44. 1 1
      system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php
  45. 10 7
      system/src/Grav/Common/Twig/Twig.php
  46. 81 28
      system/src/Grav/Common/Twig/TwigExtension.php
  47. 41 30
      system/src/Grav/Common/Uri.php
  48. 28 62
      system/src/Grav/Common/Utils.php
  49. 47 0
      system/src/Grav/Common/Yaml.php
  50. 25 13
      system/src/Grav/Console/Cli/InstallCommand.php
  51. 4 2
      system/src/Grav/Console/ConsoleTrait.php
  52. 9 6
      system/src/Grav/Console/Gpm/InstallCommand.php
  53. 5 2
      system/src/Grav/Console/Gpm/VersionCommand.php
  54. 18 9
      system/src/Grav/Framework/Cache/CacheTrait.php
  55. 0 10
      system/src/Grav/Framework/Collection/ArrayCollection.php
  56. 30 4
      system/src/Grav/Framework/ContentBlock/ContentBlock.php
  57. 11 0
      system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php
  58. 1 0
      system/src/Grav/Framework/ContentBlock/HtmlBlock.php
  59. 44 0
      system/src/Grav/Framework/File/Formatter/FormatterInterface.php
  60. 83 0
      system/src/Grav/Framework/File/Formatter/IniFormatter.php
  61. 78 0
      system/src/Grav/Framework/File/Formatter/JsonFormatter.php
  62. 116 0
      system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php
  63. 96 0
      system/src/Grav/Framework/File/Formatter/SerializeFormatter.php
  64. 103 0
      system/src/Grav/Framework/File/Formatter/YamlFormatter.php
  65. 1 11
      system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php
  66. 25 4
      system/src/Grav/Framework/Object/Base/ObjectTrait.php
  67. 198 0
      system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php
  68. 2 1
      system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php
  69. 35 0
      system/src/Grav/Framework/Object/ObjectCollection.php
  70. 4 4
      system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php
  71. 4 5
      system/src/Grav/Framework/Route/Route.php
  72. 17 0
      system/src/Grav/Framework/Route/RouteFactory.php
  73. 340 0
      system/src/Grav/Framework/Session/Session.php
  74. 147 0
      system/src/Grav/Framework/Session/SessionInterface.php
  75. 7 0
      user/pages/01._recits/_12-juillet-2017/text.md
  76. 13 0
      user/pages/01._recits/_12-septembre-2017/text.md
  77. 8 0
      user/pages/01._recits/_13-septembre-2017/text.md
  78. 24 0
      user/pages/01._recits/_19-juin-2017/text.md
  79. 7 0
      user/pages/01._recits/_25-juillet-2017/text.md
  80. 6 0
      user/pages/01._recits/_26-juillet-2017/text.md
  81. 10 0
      user/pages/01._recits/_3-mai-2017/text.md
  82. 6 0
      user/pages/01._recits/_3-octobre-2017/text.md
  83. 11 0
      user/pages/01._recits/_4-octobre-2017/text.md
  84. 12 0
      user/pages/01._recits/_5-octobre-2017/text.md
  85. 7 0
      user/pages/01._recits/_5-septembre-2017/text.md
  86. 7 0
      user/pages/01._recits/_6-juillet-2017/text.md
  87. 12 0
      user/pages/01._recits/_6-juin-2017/text.md
  88. 21 13
      user/pages/02._interviews/_andrea/text.md
  89. 2 4
      user/pages/02._interviews/_manon-dumond/text.md
  90. 12 0
      user/pages/02._interviews/_marie-w/text.md
  91. 16 0
      user/pages/02._interviews/_martine/text.md
  92. 6 0
      user/pages/02._interviews/_nadia/text.md
  93. 14 0
      user/pages/02._interviews/_oceane-et-laurent/text.md
  94. 8 0
      user/pages/02._interviews/_olivier/text.md
  95. 7 0
      user/pages/02._interviews/_pascale/text.md
  96. 11 0
      user/pages/02._interviews/_solen/text.md
  97. 14 0
      user/pages/02._interviews/_victoria/text.md
  98. BIN
      user/pages/03._images/_rue-marie-w/DSC04024.JPG
  99. 8 0
      user/pages/03._images/_rue-marie-w/image.md
  100. BIN
      user/pages/03._images/_taxiphone/DSC04028.JPG

+ 61 - 1
CHANGELOG.md

@@ -1,3 +1,63 @@
+# v1.5.1
+## 08/23/2018
+
+1. [](#new)
+    * Added static `Grav\Common\Yaml` class which should be used instead of `Symfony\Component\Yaml\Yaml`
+1. [](#improved)
+    * Updated deprecated Twig code so it works in both in Twig 1.34+ and Twig 2.4+
+    * Switched to new Grav Yaml class to support Native + Fallback YAML libraries
+1. [](#bugfix)
+    * Broken handling of user folder in Grav URI object [#2151](https://github.com/getgrav/grav/issues/2151)
+
+# v1.5.0
+## 08/17/2018
+
+1. [](#new)
+    * Set minimum requirements to [PHP 5.6.4](https://getgrav.org/blog/raising-php-requirements-2018) 
+    * Updated Doctrine Collections to 1.4
+    * Updated Symfony Components to 3.4 (with compatibility mode to fall back to Symfony YAML 2.8)
+    * Added `Uri::method()` to get current HTTP method (GET/POST etc)
+    * `FormatterInterface`: Added `getSupportedFileExtensions()` and `getDefaultFileExtension()` methods
+    * Added option to disable `SimpleCache` key validation   
+    * Added support for multiple repo locations for `bin/grav install` command 
+    * Added twig filters for casting values: `|string`, `|int`, `|bool`, `|float`, `|array`
+    * Made `ObjectCollection::matching()` criteria expressions to behave more like in Twig
+    * Criteria: Added support for `LENGTH()`, `LOWER()`, `UPPER()`, `LTRIM()`, `RTRIM()` and `TRIM()`
+    * Added `Grav\Framework\File\Formatter` classes for encoding/decoding YAML, Markdown, JSON, INI and PHP serialized strings
+    * Added `Grav\Framework\Session` class to replace `RocketTheme\Toolbox\Session\Session`
+    * Added `Grav\Common\Media` interfaces and trait; use those in `Page` and `Media` classes 
+    * Added `Grav\Common\Page` interface to allow custom page types in the future
+    * Added setting to disable sessions from the site [#2013](https://github.com/getgrav/grav/issues/2013)
+    * Added new `strict_mode` settings in `system.yaml` for compatibility
+1. [](#improved)
+    * Improved `Utils::url()` to support query strings
+    * Display better exception message if Grav fails to initialize
+    * Added `muted` and `playsinline` support to videos [#2124](https://github.com/getgrav/grav/pull/2124)
+    * Added `MediaTrait::clearMediaCache()` to allow cache to be cleared
+    * Added `MediaTrait::getMediaCache()` to allow custom caching
+    * Improved session handling, allow all session configuration options in `system.session.options`
+1. [](#bugfix)
+    * Fix broken form nonce logic [#2121](https://github.com/getgrav/grav/pull/2121)
+    * Fixed issue with uppercase extensions and fallback media URLs [#2133](https://github.com/getgrav/grav/issues/2133)   
+    * Fixed theme inheritance issue with `camel-case` that includes numbers [#2134](https://github.com/getgrav/grav/issues/2134)
+    * Typo in demo typography page [#2136](https://github.com/getgrav/grav/pull/2136)
+    * Fix for incorrect plugin order in debugger panel
+    * Made `|markdown` filter HTML safe
+    * Fixed bug in `ContentBlock` serialization
+    * Fixed `Route::withQueryParam()` to accept array values
+    * Fixed typo in truncate function [#1943](https://github.com/getgrav/grav/issues/1943)
+    * Fixed blueprint field validation: Allow numeric inputs in text fields
+
+# v1.4.8
+## 07/31/2018
+
+1. [](#improved)
+    * Add Grav version to debug bar messages tab [#2106](https://github.com/getgrav/grav/pull/2106)
+    * Add Nginx config for ddev project to `webserver-configs` [#2117](https://github.com/getgrav/grav/pull/2117)
+    * Vendor library updates
+1. [](#bugfix)
+    * Don't allow `null` to be set as Page content
+
 # v1.4.7
 ## 07/13/2018
 
@@ -6,7 +66,7 @@
 1. [](#bugfix)
     * Fix for modular page preview [#2066](https://github.com/getgrav/grav/issues/2066)
     * `Page::routeCanonical()` should be string not array [#2069](https://github.com/getgrav/grav/issues/2069)
-
+    
 # v1.4.6
 ## 06/20/2018
 

+ 1 - 1
README.md

@@ -18,7 +18,7 @@ The underlying architecture of Grav is designed to use well-established and _bes
 
 # Requirements
 
-- PHP 5.5.9 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
+- PHP 5.6.4 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
 - Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
 
 # QuickStart

+ 8 - 8
composer.json

@@ -6,17 +6,17 @@
     "homepage": "http://getgrav.org",
     "license": "MIT",
     "require": {
-        "php": ">=5.5.9",
+        "php": ">=5.6.4",
         "twig/twig": "~1.24",
         "erusev/parsedown": "1.6.4",
         "erusev/parsedown-extra": "~0.7",
-        "symfony/yaml": "~2.8",
-        "symfony/console": "~2.8",
-        "symfony/event-dispatcher": "~2.8",
-        "symfony/var-dumper": "~2.8",
+        "symfony/yaml": "~3.4",
+        "symfony/console": "~3.4",
+        "symfony/event-dispatcher": "~3.4",
+        "symfony/var-dumper": "~3.4",
         "symfony/polyfill-iconv": "~1.0",
         "doctrine/cache": "^1.6",
-        "doctrine/collections": "1.3",
+        "doctrine/collections": "^1.4",
         "psr/simple-cache": "^1.0",
         "psr/http-message": "^1.0",
         "guzzlehttp/psr7": "^1.4",
@@ -26,7 +26,7 @@
         "gregwar/image": "2.*",
         "donatj/phpuseragentparser": "~0.3",
         "pimple/pimple": "~3.2",
-        "rockettheme/toolbox": "~1.3.9",
+        "rockettheme/toolbox": "~1.4",
         "maximebf/debugbar": "~1.10",
         "ext-mbstring": "*",
         "ext-openssl": "*",
@@ -45,7 +45,7 @@
     },
     "config": {
         "platform": {
-            "php": "5.5.9"
+            "php": "5.6.4"
         }
     },
     "repositories": [

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


+ 2 - 2
index.php

@@ -7,7 +7,7 @@
  */
 
 namespace Grav;
-define('GRAV_PHP_MIN', '5.5.9');
+define('GRAV_PHP_MIN', '5.6.4');
 
 // Ensure vendor libraries exist
 $autoload = __DIR__ . '/vendor/autoload.php';
@@ -15,7 +15,7 @@ if (!is_file($autoload)) {
     die("Please run: <i>bin/grav install</i>");
 }
 
-if (PHP_SAPI == 'cli-server') {
+if (PHP_SAPI === 'cli-server') {
     if (!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>");
     }

+ 36 - 0
system/blueprints/config/system.yaml

@@ -996,6 +996,18 @@ form:
                     validate:
                         type: bool
 
+                session.initialize:
+                    type: toggle
+                    label: PLUGIN_ADMIN.SESSION_INITIALIZE
+                    help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    default: true
+                    validate:
+                        type: bool
+
                 session.timeout:
                     type: text
                     size: small
@@ -1206,3 +1218,27 @@ form:
                     placeholder: "e.g. http://yoursite.com/yourpath"
                     label: PLUGIN_ADMIN.CUSTOM_BASE_URL
                     help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
+
+                strict_mode.yaml_compat:
+                    type: toggle
+                    label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
+                    highlight: 1
+                    default: 1
+                    help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                strict_mode.twig_compat:
+                    type: toggle
+                    label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
+                    highlight: 1
+                    default: 1
+                    help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool

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

@@ -39,7 +39,6 @@ form:
         options:
           type: tab
           title: PLUGIN_ADMIN.OPTIONS
-          multiple: true
 
           fields:
 
@@ -49,7 +48,6 @@ form:
               underline: true
 
               fields:
-    
                 header.published:
                   type: toggle
                   toggleable: true

+ 1 - 0
system/blueprints/user/account.yaml

@@ -94,6 +94,7 @@ form:
                     twofa_secret:
                         type: 2fa_secret
                         outerclasses: 'twofa-secret'
+                        markdown: true
                         label: PLUGIN_ADMIN.2FA_SECRET
                         sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
 

+ 5 - 1
system/config/system.yaml

@@ -88,7 +88,7 @@ twig:
   cache: true                                    # Set to true to enable Twig caching
   debug: true                                    # Enable Twig debug
   auto_reload: true                              # Refresh cache on changes
-  autoescape: false                              # Autoescape Twig vars
+  autoescape: false                              # Autoescape Twig vars (DEPRECATED, always enabled in strict mode)
   undefined_functions: true                      # Allow undefined functions
   undefined_filters: true                        # Allow undefined filters
   umask_fix: false                               # By default Twig creates cached files as 755, fix switches this to 775
@@ -146,3 +146,7 @@ gpm:
   method: 'auto'                                 # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
   verify_peer: true                              # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
   official_gpm_only: true                        # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
+
+strict_mode:
+  yaml_compat: true                              # Grav 1.5+: Enables YAML backwards compatibility
+  twig_compat: true                              # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false)

+ 2 - 2
system/defines.php

@@ -8,12 +8,12 @@
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.4.7');
+define('GRAV_VERSION', '1.5.1');
 define('GRAV_TESTING', false);
 define('DS', '/');
 
 if (!defined('GRAV_PHP_MIN')) {
-    define('GRAV_PHP_MIN', '5.5.9');
+    define('GRAV_PHP_MIN', '5.6.4');
 }
 
 // Directories and Paths

+ 15 - 11
system/src/Grav/Common/Config/Setup.php

@@ -262,18 +262,22 @@ class Setup extends Data
             );
         }
 
-        if (!$locator->findResource('environment://config', true)) {
-            // If environment does not have its own directory, remove it from the lookup.
-            $this->set('streams.schemes.environment.prefixes', ['config' => []]);
-            $this->initializeLocator($locator);
-        }
+        try {
+            if (!$locator->findResource('environment://config', true)) {
+                // If environment does not have its own directory, remove it from the lookup.
+                $this->set('streams.schemes.environment.prefixes', ['config' => []]);
+                $this->initializeLocator($locator);
+            }
 
-        // Create security.yaml if it doesn't exist.
-        $filename = $locator->findResource('config://security.yaml', true, true);
-        $file = YamlFile::instance($filename);
-        if (!$file->exists()) {
-            $file->save(['salt' => Utils::generateRandomString(14)]);
-            $file->free();
+            // Create security.yaml if it doesn't exist.
+            $filename = $locator->findResource('config://security.yaml', true, true);
+            $file = YamlFile::instance($filename);
+            if (!$file->exists()) {
+                $file->save(['salt' => Utils::generateRandomString(14)]);
+                $file->free();
+            }
+        } catch (\RuntimeException $e) {
+            throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
         }
     }
 }

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

@@ -10,9 +10,8 @@ namespace Grav\Common\Data;
 
 use Grav\Common\Grav;
 use Grav\Common\Utils;
-use Symfony\Component\Yaml\Exception\ParseException;
-use Symfony\Component\Yaml\Parser;
-use Symfony\Component\Yaml\Yaml;
+use Grav\Common\Yaml;
+use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYaml;
 
 class Validation
 {
@@ -107,7 +106,7 @@ class Validation
         $method = 'filter' . ucfirst(strtr($type, '-', '_'));
 
         // If this is a YAML field validate/filter as such
-        if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
+        if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
             $method = 'filterYaml';
         }
 
@@ -128,10 +127,12 @@ class Validation
      */
     public static function typeText($value, array $params, array $field)
     {
-        if (!is_string($value)) {
+        if (!is_string($value) && !is_numeric($value)) {
             return false;
         }
 
+        $value = (string)$value;
+
         if (isset($params['min']) && strlen($value) < $params['min']) {
             return false;
         }
@@ -643,15 +644,12 @@ class Validation
 
     public static function filterYaml($value, $params)
     {
-        try {
-            if (is_string($value)) {
-                return (array) Yaml::parse($value);
-            } else {
-                return $value;
-            }
-        } catch (ParseException $e) {
+        if (!is_string($value)) {
             return $value;
         }
+
+        return (array) Yaml::parse($value);
+
     }
 
     /**

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

@@ -58,8 +58,15 @@ class Debugger
         $this->enabled = $this->config->get('system.debugger.enabled');
 
         if ($this->enabled()) {
+
+            $plugins_config = (array)$this->config->get('plugins');
+
+            ksort($plugins_config);
+
+
             $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
-            $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('plugins'), 'Plugins'));
+            $this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
+            $this->addMessage('Grav v' . GRAV_VERSION);
         }
 
         return $this;

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

@@ -20,9 +20,6 @@ trait CompiledFile
      */
     public function content($var = null)
     {
-        // Set some options
-        $this->settings(['native' => true, 'compat' => true]);
-
         try {
             // If nothing has been loaded, attempt to get pre-compiled version of the file first.
             if ($var === null && $this->raw === null && $this->content === null) {

+ 8 - 3
system/src/Grav/Common/GPM/GPM.php

@@ -13,7 +13,7 @@ use Grav\Common\Filesystem\Folder;
 use Grav\Common\Inflector;
 use Grav\Common\Iterator;
 use Grav\Common\Utils;
-use Symfony\Component\Yaml\Yaml;
+use RocketTheme\Toolbox\File\YamlFile;
 
 class GPM extends Iterator
 {
@@ -624,7 +624,10 @@ class GPM extends Iterator
             return false;
         }
 
-        $blueprint = (array)Yaml::parse(file_get_contents($blueprint_file));
+        $file = YamlFile::instance($blueprint_file);
+        $blueprint = (array)$file->content();
+        $file->free();
+
         return $blueprint;
     }
 
@@ -873,7 +876,9 @@ class GPM extends Iterator
                 // get currently installed version
                 $locator = Grav::instance()['locator'];
                 $blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml');
-                $package_yaml = Yaml::parse(file_get_contents($blueprints_path));
+                $file = YamlFile::instance($blueprints_path);
+                $package_yaml = $file->content();
+                $file->free();
                 $currentlyInstalledVersion = $package_yaml['version'];
 
                 // if requirement is next significant release, check is compatible with currently installed version, might not be

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

@@ -114,7 +114,7 @@ class Licenses
 
     {
         if (!isset(self::$file)) {
-            $path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';;
+            $path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';
             if (!file_exists($path)) {
                 touch($path);
             }

+ 3 - 7
system/src/Grav/Common/Grav.php

@@ -9,7 +9,6 @@
 namespace Grav\Common;
 
 use Grav\Common\Config\Config;
-use Grav\Common\Language\Language;
 use Grav\Common\Page\Medium\ImageMedium;
 use Grav\Common\Page\Medium\Medium;
 use Grav\Common\Page\Page;
@@ -205,11 +204,8 @@ class Grav extends Container
      */
     public function redirectLangSafe($route, $code = null)
     {
-        /** @var Language $language */
-        $language = $this['language'];
-
-        if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) {
-            $this->redirect($language->getLanguage() . $route, $code);
+        if (!$this['uri']->isExternal($route)) {
+            $this->redirect($this['pages']->route($route), $code);
         } else {
             $this->redirect($route, $code);
         }
@@ -443,7 +439,7 @@ class Grav extends Container
         /** @var Config $config */
         $config = $this['config'];
 
-        $uri_extension = $uri->extension();
+        $uri_extension = strtolower($uri->extension());
         $fallback_types = $config->get('system.media.allowed_fallback_types', null);
         $supported_types = $config->get('media.types');
 

+ 4 - 3
system/src/Grav/Common/Inflector.php

@@ -190,10 +190,11 @@ class Inflector
     public function hyphenize($word)
     {
         $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word);
-        $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1-\2', $regex1);
-        $regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex2);
+        $regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1);
+        $regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2);
+        $regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3);
 
-        return strtolower($regex3);
+        return strtolower($regex4);
     }
 
     /**

+ 9 - 0
system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php

@@ -0,0 +1,9 @@
+<?php
+namespace Grav\Common\Media\Interfaces;
+
+/**
+ * Class implements media collection interface.
+ */
+interface MediaCollectionInterface
+{
+}

+ 29 - 0
system/src/Grav/Common/Media/Interfaces/MediaInterface.php

@@ -0,0 +1,29 @@
+<?php
+namespace Grav\Common\Media\Interfaces;
+
+/**
+ * Class implements media interface.
+ */
+interface MediaInterface
+{
+    /**
+     * Gets the associated media collection.
+     *
+     * @return MediaCollectionInterface  Collection of associated media.
+     */
+    public function getMedia();
+
+    /**
+     * Get filesystem path to the associated media.
+     *
+     * @return string|null  Media path or null if the object doesn't have media folder.
+     */
+    public function getMediaFolder();
+
+    /**
+     * Get display order for the associated media.
+     *
+     * @return array Empty array means default ordering.
+     */
+    public function getMediaOrder();
+}

+ 9 - 0
system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php

@@ -0,0 +1,9 @@
+<?php
+namespace Grav\Common\Media\Interfaces;
+
+/**
+ * Class implements media object interface.
+ */
+interface MediaObjectInterface
+{
+}

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

@@ -0,0 +1,112 @@
+<?php
+namespace Grav\Common\Media\Traits;
+
+use Grav\Common\Cache;
+use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\MediaCollectionInterface;
+use Grav\Common\Page\Media;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+trait MediaTrait
+{
+    protected $media;
+
+    /**
+     * Get filesystem path to the associated media.
+     *
+     * @return string|null
+     */
+    abstract public function getMediaFolder();
+
+    /**
+     * Get display order for the associated media.
+     *
+     * @return array Empty array means default ordering.
+     */
+    abstract public function getMediaOrder();
+
+    /**
+     * Get URI ot the associated media. Method will return null if path isn't URI.
+     *
+     * @return null|string
+     */
+    public function getMediaUri()
+    {
+       $folder = $this->getMediaFolder();
+
+       if (strpos($folder, '://')) {
+           return $folder;
+       }
+
+       /** @var UniformResourceLocator $locator */
+       $locator = Grav::instance()['locator'];
+       $user = $locator->findResource('user://');
+       if (strpos($folder, $user) === 0) {
+           return 'user://' . substr($folder, strlen($user)+1);
+       }
+
+       return null;
+    }
+
+    /**
+     * Gets the associated media collection.
+     *
+     * @return MediaCollectionInterface  Representation of associated media.
+     */
+    public function getMedia()
+    {
+        $cache = $this->getMediaCache();
+
+        if ($this->media === null) {
+            // Use cached media if possible.
+            $cacheKey = md5('media' . $this->getCacheKey());
+            if (!$media = $cache->fetch($cacheKey)) {
+                $media = new Media($this->getMediaFolder(), $this->getMediaOrder());
+                $cache->save($cacheKey, $media);
+            }
+            $this->media = $media;
+        }
+
+        return $this->media;
+    }
+
+    /**
+     * Sets the associated media collection.
+     *
+     * @param  MediaCollectionInterface  $media Representation of associated media.
+     * @return $this
+     */
+    protected function setMedia(MediaCollectionInterface $media)
+    {
+        $cache = $this->getMediaCache();
+        $cacheKey = md5('media' . $this->getCacheKey());
+        $cache->save($cacheKey, $media);
+
+        $this->media = $media;
+
+        return $this;
+    }
+
+    /**
+     * Clear media cache.
+     */
+    protected function clearMediaCache()
+    {
+        $cache = $this->getMediaCache();
+        $cacheKey = md5('media' . $this->getCacheKey());
+        $cache->delete($cacheKey);
+    }
+
+    /**
+     * @return Cache
+     */
+    protected function getMediaCache()
+    {
+        return Grav::instance()['cache'];
+    }
+
+    /**
+     * @return string
+     */
+    abstract protected function getCacheKey();
+}

+ 9 - 0
system/src/Grav/Common/Page/Interfaces/PageInterface.php

@@ -0,0 +1,9 @@
+<?php
+namespace Grav\Common\Page\Interfaces;
+
+/**
+ * Class implements page interface.
+ */
+interface PageInterface
+{
+}

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

@@ -9,11 +9,11 @@
 namespace Grav\Common\Page;
 
 use Grav\Common\Grav;
+use Grav\Common\Yaml;
 use Grav\Common\Page\Medium\AbstractMedia;
 use Grav\Common\Page\Medium\GlobalMedia;
 use Grav\Common\Page\Medium\MediumFactory;
 use RocketTheme\Toolbox\File\File;
-use Symfony\Component\Yaml\Yaml;
 
 class Media extends AbstractMedia
 {
@@ -24,11 +24,13 @@ class Media extends AbstractMedia
     protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];
 
     /**
-     * @param $path
+     * @param string $path
+     * @param array  $media_order
      */
-    public function __construct($path)
+    public function __construct($path, array $media_order = null)
     {
         $this->path = $path;
+        $this->media_order = $media_order;
 
         $this->__wakeup();
         $this->init();

+ 20 - 11
system/src/Grav/Common/Page/Medium/AbstractMedia.php

@@ -10,9 +10,11 @@ namespace Grav\Common\Page\Medium;
 
 use Grav\Common\Getters;
 use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\MediaCollectionInterface;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
 use Grav\Common\Utils;
 
-abstract class AbstractMedia extends Getters
+abstract class AbstractMedia extends Getters implements MediaCollectionInterface
 {
     protected $gettersVariable = 'instances';
 
@@ -21,6 +23,7 @@ abstract class AbstractMedia extends Getters
     protected $videos = [];
     protected $audios = [];
     protected $files = [];
+    protected $media_order;
 
     /**
      * Get medium by filename.
@@ -62,7 +65,7 @@ abstract class AbstractMedia extends Getters
     /**
      * Get a list of all media.
      *
-     * @return array|Medium[]
+     * @return array|MediaObjectInterface[]
      */
     public function all()
     {
@@ -74,7 +77,7 @@ abstract class AbstractMedia extends Getters
     /**
      * Get a list of all image media.
      *
-     * @return array|Medium[]
+     * @return array|MediaObjectInterface[]
      */
     public function images()
     {
@@ -85,7 +88,7 @@ abstract class AbstractMedia extends Getters
     /**
      * Get a list of all video media.
      *
-     * @return array|Medium[]
+     * @return array|MediaObjectInterface[]
      */
     public function videos()
     {
@@ -96,7 +99,7 @@ abstract class AbstractMedia extends Getters
     /**
      * Get a list of all audio media.
      *
-     * @return array|Medium[]
+     * @return array|MediaObjectInterface[]
      */
     public function audios()
     {
@@ -107,7 +110,7 @@ abstract class AbstractMedia extends Getters
     /**
      * Get a list of all file media.
      *
-     * @return array|Medium[]
+     * @return array|MediaObjectInterface[]
      */
     public function files()
     {
@@ -117,7 +120,7 @@ abstract class AbstractMedia extends Getters
 
     /**
      * @param string $name
-     * @param Medium $file
+     * @param MediaObjectInterface $file
      */
     protected function add($name, $file)
     {
@@ -145,14 +148,20 @@ abstract class AbstractMedia extends Getters
      */
     protected function orderMedia($media)
     {
-        $page = Grav::instance()['pages']->get($this->path);
+        if (null === $this->media_order) {
+            $page = Grav::instance()['pages']->get($this->path);
 
-        if ($page && isset($page->header()->media_order)) {
-            $media_order = array_map('trim', explode(',', $page->header()->media_order));
-            $media = Utils::sortArrayByArray($media, $media_order);
+            if ($page && isset($page->header()->media_order)) {
+                $this->media_order = array_map('trim', explode(',', $page->header()->media_order));
+            }
+        }
+
+        if (!empty($this->media_order) && is_array($this->media_order)) {
+            $media = Utils::sortArrayByArray($media, $this->media_order);
         } else {
             ksort($media, SORT_NATURAL | SORT_FLAG_CASE);
         }
+
         return $media;
     }
 

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

@@ -11,6 +11,7 @@ namespace Grav\Common\Page\Medium;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Grav;
 use Grav\Common\Utils;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 class ImageMedium extends Medium
 {
@@ -164,12 +165,18 @@ class ImageMedium extends Medium
      */
     public function url($reset = true)
     {
-        $image_path = Grav::instance()['locator']->findResource('cache://images', true);
-        $image_dir = Grav::instance()['locator']->findResource('cache://images', false);
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+        $image_path = $locator->findResource('cache://images', true);
+        $image_dir = $locator->findResource('cache://images', false);
         $saved_image_path = $this->saveImage();
 
         $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
 
+        if ($locator->isStream($output)) {
+            $output = $locator->findResource($output, false);
+        }
+
         if (Utils::startsWith($output, $image_path)) {
             $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
         }

+ 8 - 3
system/src/Grav/Common/Page/Medium/Medium.php

@@ -12,9 +12,9 @@ use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Grav;
 use Grav\Common\Data\Data;
 use Grav\Common\Data\Blueprint;
-use Grav\Common\Utils;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
 
-class Medium extends Data implements RenderableInterface
+class Medium extends Data implements RenderableInterface, MediaObjectInterface
 {
     use ParsedownHtmlTrait;
 
@@ -199,7 +199,12 @@ class Medium extends Data implements RenderableInterface
      */
     public function url($reset = true)
     {
-        $output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $this->get('filepath'));
+        $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
+
+        $locator = Grav::instance()['locator'];
+        if ($locator->isStream($output)) {
+            $output = $locator->findResource($output, false);
+        }
 
         if ($reset) {
             $this->reset();

+ 34 - 0
system/src/Grav/Common/Page/Medium/VideoMedium.php

@@ -94,6 +94,40 @@ class VideoMedium extends Medium
         return $this;
     }
 
+    /**
+     * Allows to set the playsinline attribute
+     *
+     * @param bool $status
+     * @return $this
+     */
+    public function playsinline($status = false)
+    {
+        if($status) {
+            $this->attributes['playsinline'] = true;
+        } else {
+            unset($this->attributes['playsinline']);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Allows to set the muted attribute
+     *
+     * @param bool $status
+     * @return $this
+     */
+    public function muted($status = false)
+    {
+        if($status) {
+            $this->attributes['muted'] = true;
+        } else {
+            unset($this->attributes['muted']);
+        }
+
+        return $this;
+    }
+
     /**
      * Reset medium.
      *

+ 62 - 52
system/src/Grav/Common/Page/Page.php

@@ -12,23 +12,26 @@ use Exception;
 use Grav\Common\Cache;
 use Grav\Common\Config\Config;
 use Grav\Common\Data\Blueprint;
+use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Filesystem\Folder;
 use Grav\Common\Grav;
-use Grav\Common\Language\Language;
 use Grav\Common\Markdown\Parsedown;
 use Grav\Common\Markdown\ParsedownExtra;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Media\Traits\MediaTrait;
 use Grav\Common\Taxonomy;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
+use Grav\Common\Yaml;
 use RocketTheme\Toolbox\Event\Event;
 use RocketTheme\Toolbox\File\MarkdownFile;
-use Symfony\Component\Yaml\Exception\ParseException;
-use Symfony\Component\Yaml\Yaml;
 
 define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
 
-class Page
+class Page implements PageInterface
 {
+    use MediaTrait;
+
     /**
      * @var string Filename. Leave as null if page is folder.
      */
@@ -65,7 +68,6 @@ class Page
     protected $summary;
     protected $raw_content;
     protected $pagination;
-    protected $media;
     protected $metadata;
     protected $title;
     protected $max_count;
@@ -318,8 +320,6 @@ class Page
         if (!$this->header) {
             $file = $this->file();
             if ($file) {
-                // Set some options
-                $file->settings(['native' => true, 'compat' => true]);
                 try {
                     $this->raw_content = $file->markdown();
                     $this->frontmatter = $file->frontmatter();
@@ -328,11 +328,12 @@ class Page
                     if (!Utils::isAdminPlugin()) {
                         // If there's a `frontmatter.yaml` file merge that in with the page header
                         // note page's own frontmatter has precedence and will overwrite any defaults
-                        $frontmatter_file = $this->path . '/' . $this->folder . '/frontmatter.yaml';
-                        if (file_exists($frontmatter_file)) {
-                            $frontmatter_data = (array)Yaml::parse(file_get_contents($frontmatter_file));
+                        $frontmatterFile = CompiledYamlFile::instance($this->path . '/' . $this->folder . '/frontmatter.yaml');
+                        if ($frontmatterFile->exists()) {
+                            $frontmatter_data = (array)$frontmatterFile->content();
                             $this->header = (object)array_replace_recursive($frontmatter_data,
                                 (array)$this->header);
+                            $frontmatterFile->free();
                         }
                         // Process frontmatter with Twig if enabled
                         if (Grav::instance()['config']->get('system.pages.frontmatter.process_twig') === true) {
@@ -813,6 +814,8 @@ class Page
      */
     public function setRawContent($content)
     {
+        $content = $content === null ? '': $content;
+
         $this->content = $content;
     }
 
@@ -1122,6 +1125,14 @@ class Page
         return json_encode($this->toArray());
     }
 
+    /**
+     * @return string
+     */
+    protected function getCacheKey()
+    {
+        return $this->id();
+    }
+
     /**
      * Gets and sets the associated media as found in the page folder.
      *
@@ -1131,23 +1142,33 @@ class Page
      */
     public function media($var = null)
     {
-        /** @var Cache $cache */
-        $cache = Grav::instance()['cache'];
-
         if ($var) {
-            $this->media = $var;
-        }
-        if ($this->media === null) {
-            // Use cached media if possible.
-            $media_cache_id = md5('media' . $this->id());
-            if (!$media = $cache->fetch($media_cache_id)) {
-                $media = new Media($this->path());
-                $cache->save($media_cache_id, $media);
-            }
-            $this->media = $media;
+            $this->setMedia($var);
         }
 
-        return $this->media;
+        return $this->getMedia();
+    }
+
+    /**
+     * Get filesystem path to the associated media.
+     *
+     * @return string|null
+     */
+    public function getMediaFolder()
+    {
+        return $this->path();
+    }
+
+    /**
+     * Get display order for the associated media.
+     *
+     * @return array Empty array means default ordering.
+     */
+    public function getMediaOrder()
+    {
+        $header = $this->header();
+
+        return isset($header->media_order) ? array_map('trim', explode(',', (string)$header->media_order)) : [];
     }
 
     /**
@@ -1626,14 +1647,19 @@ class Page
      * Gets the url for the Page.
      *
      * @param bool $include_host Defaults false, but true would include http://yourhost.com
-     * @param bool $canonical true to return the canonical URL
-     * @param bool $include_lang
+     * @param bool $canonical    True to return the canonical URL
+     * @param bool $include_base Include base url on multisite as well as language code
      * @param bool $raw_route
      *
      * @return string The url.
      */
-    public function url($include_host = false, $canonical = false, $include_lang = true, $raw_route = false)
+    public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false)
     {
+        // Override any URL when external_url is set
+        if (isset($this->external_url)) {
+            return $this->external_url;
+        }
+
         $grav = Grav::instance();
 
         /** @var Pages $pages */
@@ -1642,41 +1668,25 @@ class Page
         /** @var Config $config */
         $config = $grav['config'];
 
-        /** @var Language $language */
-        $language = $grav['language'];
-
-        /** @var Uri $uri */
-        $uri = $grav['uri'];
-
-        // Override any URL when external_url is set
-        if (isset($this->external_url)) {
-            return $this->external_url;
-        }
-
-        // get pre-route
-        if ($include_lang && $language->enabled()) {
-            $pre_route = $language->getLanguageURLPrefix();
-        } else {
-            $pre_route = '';
-        }
+        // get base route (multisite base and language)
+        $route = $include_base ? $pages->baseRoute() : '';
 
         // add full route if configured to do so
-        if ($config->get('system.absolute_urls', false)) {
+        if (!$include_host && $config->get('system.absolute_urls', false)) {
             $include_host = true;
         }
 
-        // get canonical route if requested
         if ($canonical) {
-            $route = $pre_route . $this->routeCanonical();
+            $route .= $this->routeCanonical();
         } elseif ($raw_route) {
-            $route = $pre_route . $this->rawRoute();
+            $route .= $this->rawRoute();
         } else {
-            $route = $pre_route . $this->route();
+            $route .= $this->route();
         }
 
-        $rootUrl = $uri->rootUrl($include_host) . $pages->base();
-
-        $url = $rootUrl . '/' . trim($route, '/') . $this->urlExtension();
+        /** @var Uri $uri */
+        $uri = $grav['uri'];
+        $url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
 
         // trim trailing / if not root
         if ($url !== '/') {

+ 46 - 24
system/src/Grav/Common/Page/Pages.php

@@ -49,7 +49,7 @@ class Pages
     /**
      * @var array|string[]
      */
-    protected $baseUrl = [];
+    protected $baseRoute = [];
 
     /**
      * @var array|string[]
@@ -120,7 +120,7 @@ class Pages
         if ($path !== null) {
             $path = trim($path, '/');
             $this->base = $path ? '/' . $path : null;
-            $this->baseUrl = [];
+            $this->baseRoute = [];
         }
 
         return $this->base;
@@ -128,39 +128,61 @@ class Pages
 
     /**
      *
-     * Get base URL for Grav pages.
+     * Get base route for Grav pages.
      *
-     * @param  string $lang     Optional language code for multilingual links.
-     * @param  bool   $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
+     * @param  string $lang     Optional language code for multilingual routes.
      *
      * @return string
      */
-    public function baseUrl($lang = null, $absolute = null)
+    public function baseRoute($lang = null)
     {
-        $lang = (string) $lang;
-        $type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative');
-        $key = "{$lang} {$type}";
-
-        if (!isset($this->baseUrl[$key])) {
-            /** @var Config $config */
-            $config = $this->grav['config'];
+        $key = $lang ?: 'default';
 
+        if (!isset($this->baseRoute[$key])) {
             /** @var Language $language */
             $language = $this->grav['language'];
 
-            if (!$lang) {
-                $lang = $language->getActive();
-            }
+            $path_base = rtrim($this->base(), '/');
+            $path_lang = $language->enabled() ? $language->getLanguageURLPrefix($lang) : '';
 
-            $path_append = rtrim($this->grav['pages']->base(), '/');
-            if ($language->getDefault() !== $lang || $config->get('system.languages.include_default_lang') === true) {
-                $path_append .= $lang ? '/' . $lang : '';
-            }
+            $this->baseRoute[$key] = $path_base . $path_lang;
+        }
+
+        return $this->baseRoute[$key];
+    }
 
-            $this->baseUrl[$key] = $this->grav[$type] . $path_append;
+    /**
+     *
+     * Get route for Grav site.
+     *
+     * @param  string $route    Optional route to the page.
+     * @param  string $lang     Optional language code for multilingual links.
+     *
+     * @return string
+     */
+    public function route($route = '/', $lang = null)
+    {
+        if (!$route || $route === '/') {
+            return $this->baseRoute($lang) ?: '/';
         }
 
-        return $this->baseUrl[$key];
+        return $this->baseRoute($lang) . $route;
+    }
+
+    /**
+     *
+     * Get base URL for Grav pages.
+     *
+     * @param  string     $lang     Optional language code for multilingual links.
+     * @param  bool|null  $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
+     *
+     * @return string
+     */
+    public function baseUrl($lang = null, $absolute = null)
+    {
+        $type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative');
+
+        return $this->grav[$type] . $this->baseRoute($lang);
     }
 
     /**
@@ -179,7 +201,7 @@ class Pages
 
     /**
      *
-     * Get home URL for Grav site.
+     * Get URL for Grav site.
      *
      * @param  string $route    Optional route to the page.
      * @param  string $lang     Optional language code for multilingual links.
@@ -189,7 +211,7 @@ class Pages
      */
     public function url($route = '/', $lang = null, $absolute = null)
     {
-        if ($route === '/') {
+        if (!$route || $route === '/') {
             return $this->homeUrl($lang, $absolute);
         }
 

+ 20 - 9
system/src/Grav/Common/Processors/InitializeProcessor.php

@@ -8,6 +8,10 @@
 
 namespace Grav\Common\Processors;
 
+use Grav\Common\Config\Config;
+use Grav\Common\Uri;
+use Grav\Common\Utils;
+
 class InitializeProcessor extends ProcessorBase implements ProcessorInterface
 {
     public $id = 'init';
@@ -15,29 +19,36 @@ class InitializeProcessor extends ProcessorBase implements ProcessorInterface
 
     public function process()
     {
-        $this->container['config']->debug();
+        /** @var Config $config */
+        $config = $this->container['config'];
+        $config->debug();
 
         // Use output buffering to prevent headers from being sent too early.
         ob_start();
-        if ($this->container['config']->get('system.cache.gzip')) {
+        if ($config->get('system.cache.gzip') && !@ob_start('ob_gzhandler')) {
             // Enable zip/deflate with a fallback in case of if browser does not support compressing.
-            if (!@ob_start("ob_gzhandler")) {
-                ob_start();
-            }
+            ob_start();
         }
 
         // Initialize the timezone.
-        if ($this->container['config']->get('system.timezone')) {
+        if ($config->get('system.timezone')) {
             date_default_timezone_set($this->container['config']->get('system.timezone'));
         }
 
         // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
-        if ($this->container['config']->get('system.session.initialize', 1) && isset($this->container['session'])) {
+        if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
             $this->container['session']->init();
         }
 
-        // Initialize uri.
-        $this->container['uri']->init();
+        /** @var Uri $uri */
+        $uri = $this->container['uri'];
+        $uri->init();
+
+        // Redirect pages with trailing slash if configured to do so.
+        $path = $uri->path() ?: '/';
+        if ($path !== '/' && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($path, '/')) {
+            $this->container->redirect(rtrim($path, '/'), 302);
+        }
 
         $this->container->setLocale();
     }

+ 13 - 1
system/src/Grav/Common/Service/ConfigServiceProvider.php

@@ -16,6 +16,7 @@ use Grav\Common\Config\ConfigFileFinder;
 use Grav\Common\Config\Setup;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
+use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 class ConfigServiceProvider implements ServiceProviderInterface
@@ -31,7 +32,14 @@ class ConfigServiceProvider implements ServiceProviderInterface
         };
 
         $container['config'] = function ($c) {
-            return static::load($c);
+            $config = static::load($c);
+
+            // After configuration has been loaded, we can disable YAML compatibility if strict mode has been enabled.
+            if (!$config->get('system.strict_mode.yaml_compat', true)) {
+                YamlFile::globalSettings(['compat' => false, 'native' => true]);
+            }
+
+            return $config;
         };
 
         $container['languages'] = function ($c) {
@@ -65,6 +73,10 @@ class ConfigServiceProvider implements ServiceProviderInterface
         return $blueprints->name("master-{$setup->environment}")->load();
     }
 
+    /**
+     * @param Container $container
+     * @return Config
+     */
     public static function load(Container $container)
     {
         /** Setup $setup */

+ 17 - 20
system/src/Grav/Common/Service/PageServiceProvider.php

@@ -8,6 +8,7 @@
 
 namespace Grav\Common\Service;
 
+use Grav\Common\Config\Config;
 use Grav\Common\Grav;
 use Grav\Common\Language\Language;
 use Grav\Common\Page\Page;
@@ -26,35 +27,33 @@ class PageServiceProvider implements ServiceProviderInterface
             /** @var Pages $pages */
             $pages = $c['pages'];
 
+            /** @var Config $config */
+            $config = $c['config'];
+
             /** @var Uri $uri */
             $uri = $c['uri'];
 
-            $path = $uri->path(); // Don't trim to support trailing slash default routes
-            $path = $path ?: '/';
-
+            $path = $uri->path() ?: '/'; // Don't trim to support trailing slash default routes
             $page = $pages->dispatch($path);
 
             // Redirection tests
             if ($page) {
-                /** @var Language $language */
-                $language = $c['language'];
-
                 // some debugger override logic
                 if ($page->debugger() === false) {
                     $c['debugger']->enabled(false);
                 }
 
-                if ($c['config']->get('system.force_ssl')) {
-                    if (!isset($_SERVER['HTTPS']) || $_SERVER["HTTPS"] != "on") {
-                        $url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
+                if ($config->get('system.force_ssl')) {
+                    if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
+                        $url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
                         $c->redirect($url);
                     }
                 }
 
-                $url = $page->route();
+                $url = $pages->route($page->route());
 
                 if ($uri->params()) {
-                    if ($url == '/') { //Avoid double slash
+                    if ($url === '/') { //Avoid double slash
                         $url = $uri->params();
                     } else {
                         $url .= $uri->params();
@@ -67,18 +66,16 @@ class PageServiceProvider implements ServiceProviderInterface
                     $url .= '#' . $uri->fragment();
                 }
 
+                /** @var Language $language */
+                $language = $c['language'];
+
                 // Language-specific redirection scenarios
-                if ($language->enabled()) {
-                    if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) {
-                        $c->redirect($url);
-                    }
-                    if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) {
-                        $c->redirectLangSafe($url);
-                    }
+                if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
+                    $c->redirect($url);
                 }
                 // Default route test and redirect
-                if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) {
-                    $c->redirectLangSafe($url);
+                if ($config->get('system.pages.redirect_default_route') && $page->route() !== $path) {
+                    $c->redirect($url);
                 }
             }
 

+ 27 - 20
system/src/Grav/Common/Service/SessionServiceProvider.php

@@ -29,21 +29,22 @@ class SessionServiceProvider implements ServiceProviderInterface
             /** @var Uri $uri */
             $uri = $c['uri'];
 
-            // Get session parameters.
-            $session_timeout = (int)$config->get('system.session.timeout', 1800);
-            $session_path = $config->get('system.session.path');
-            if (null === $session_path) {
-                $session_path = '/' . ltrim(Uri::filterPath($uri->rootUrl(false)), '/');
-            }
-            $domain = $uri->host();
-            if ($domain === 'localhost') {
-                $domain = '';
-            }
-
             // Get session options.
-            $secure = (bool)$config->get('system.session.secure', false);
-            $httponly = (bool)$config->get('system.session.httponly', true);
             $enabled = (bool)$config->get('system.session.enabled', false);
+            $cookie_secure = (bool)$config->get('system.session.secure', false);
+            $cookie_httponly = (bool)$config->get('system.session.httponly', true);
+            $cookie_lifetime = (int)$config->get('system.session.timeout', 1800);
+            $cookie_path = $config->get('system.session.path');
+            if (null === $cookie_path) {
+                $cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/');
+            }
+            // Session cookie path requires trailing slash.
+            $cookie_path = rtrim($cookie_path, '/') . '/';
+
+            $cookie_domain = $uri->host();
+            if ($cookie_domain === 'localhost') {
+                $cookie_domain = '';
+            }
 
             // Activate admin if we're inside the admin path.
             $is_admin = false;
@@ -56,14 +57,14 @@ class SessionServiceProvider implements ServiceProviderInterface
                 // Check no language, simple language prefix (en) and region specific language prefix (en-US).
                 $pos = strpos($current_route, $base);
                 if ($pos === 0 || $pos === 3 || $pos === 6) {
-                    $session_timeout = $config->get('plugins.admin.session.timeout', 1800);
+                    $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800);
                     $enabled = $is_admin = true;
                 }
             }
 
             // Fix for HUGE session timeouts.
-            if ($session_timeout > 99999999999) {
-                $session_timeout = 9999999999;
+            if ($cookie_lifetime > 99999999999) {
+                $cookie_lifetime = 9999999999;
             }
 
             $inflector = new Inflector();
@@ -73,10 +74,16 @@ class SessionServiceProvider implements ServiceProviderInterface
             }
 
             // Define session service.
-            $session = new Session($session_timeout, $session_path, $domain);
-            $session->setName($session_name);
-            $session->setSecure($secure);
-            $session->setHttpOnly($httponly);
+            $options = [
+                'name' => $session_name,
+                'cookie_lifetime' => $cookie_lifetime,
+                'cookie_path' => $cookie_path,
+                'cookie_domain' => $cookie_domain,
+                'cookie_secure' => $cookie_secure,
+                'cookie_httponly' => $cookie_httponly
+            ] + (array) $config->get('system.session.options');
+
+            $session = new Session($options);
             $session->setAutoStart($enabled);
 
             return $session;

+ 17 - 38
system/src/Grav/Common/Session.php

@@ -8,34 +8,18 @@
 
 namespace Grav\Common;
 
-use RocketTheme\Toolbox\Session\Session as BaseSession;
-
-class Session extends BaseSession
+class Session extends \Grav\Framework\Session\Session
 {
     /** @var bool */
     protected $autoStart = false;
 
-    protected $lifetime;
-    protected $path;
-    protected $domain;
-    protected $secure;
-    protected $httpOnly;
-
     /**
-     * @param int    $lifetime Defaults to 1800 seconds.
-     * @param string $path     Cookie path.
-     * @param string $domain   Optional, domain for the session
-     * @throws \RuntimeException
+     * @return \Grav\Framework\Session\Session
+     * @deprecated 1.5
      */
-    public function __construct($lifetime, $path, $domain = null)
+    public static function instance()
     {
-        $this->lifetime = $lifetime;
-        $this->path = $path;
-        $this->domain = $domain;
-
-        if (php_sapi_name() !== 'cli') {
-            parent::__construct($lifetime, $path, $domain);
-        }
+        return static::getInstance();
     }
 
     /**
@@ -48,9 +32,6 @@ class Session extends BaseSession
         if ($this->autoStart) {
             $this->start();
 
-            // TODO: This setcookie shouldn't be here, session should by itself be able to update its cookie.
-            setcookie(session_name(), session_id(), $this->lifetime ? time() + $this->lifetime : 0, $this->path, $this->domain, $this->secure, $this->httpOnly);
-
             $this->autoStart = false;
         }
     }
@@ -67,27 +48,25 @@ class Session extends BaseSession
     }
 
     /**
-     * @param bool $secure
-     * @return $this
+     * Returns attributes.
+     *
+     * @return array Attributes
+     * @deprecated 1.5
      */
-    public function setSecure($secure)
+    public function all()
     {
-        $this->secure = $secure;
-        ini_set('session.cookie_secure', (bool)$secure);
-
-        return $this;
+        return $this->getAll();
     }
 
     /**
-     * @param bool $httpOnly
-     * @return $this
+     * Checks if the session was started.
+     *
+     * @return Boolean
+     * @deprecated 1.5
      */
-    public function setHttpOnly($httpOnly)
+    public function started()
     {
-        $this->httpOnly = $httpOnly;
-        ini_set('session.cookie_httponly', (bool)$httpOnly);
-
-        return $this;
+        return $this->isStarted();
     }
 
     /**

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

@@ -12,7 +12,7 @@ class TwigNodeMarkdown extends \Twig_Node implements \Twig_NodeOutputInterface
 {
     public function __construct(\Twig_Node $body, $lineno, $tag = 'markdown')
     {
-        parent::__construct(array('body' => $body), array(), $lineno, $tag);
+        parent::__construct(['body' => $body], [], $lineno, $tag);
     }
     /**
      * Compiles the node to PHP.

+ 3 - 3
system/src/Grav/Common/Twig/Node/TwigNodeScript.php

@@ -14,7 +14,7 @@ class TwigNodeScript extends \Twig_Node implements \Twig_NodeOutputInterface
 
     /**
      * TwigNodeScript constructor.
-     * @param \Twig_NodeInterface|null $body
+     * @param \Twig_Node|null $body
      * @param \Twig_Node_Expression|null $file
      * @param \Twig_Node_Expression|null $group
      * @param \Twig_Node_Expression|null $priority
@@ -23,12 +23,12 @@ class TwigNodeScript extends \Twig_Node implements \Twig_NodeOutputInterface
      * @param string|null $tag
      */
     public function __construct(
-        \Twig_NodeInterface $body = null,
+        \Twig_Node $body = null,
         \Twig_Node_Expression $file = null,
         \Twig_Node_Expression $group = null,
         \Twig_Node_Expression $priority = null,
         \Twig_Node_Expression $attributes = null,
-        $lineno,
+        $lineno = 0,
         $tag = null
     )
     {

+ 3 - 3
system/src/Grav/Common/Twig/Node/TwigNodeStyle.php

@@ -14,18 +14,18 @@ class TwigNodeStyle extends \Twig_Node implements \Twig_NodeOutputInterface
 
     /**
      * TwigNodeAssets constructor.
-     * @param \Twig_NodeInterface|null $body
+     * @param \Twig_Node|null $body
      * @param \Twig_Node_Expression|null $attributes
      * @param int $lineno
      * @param null $tag
      */
     public function __construct(
-        \Twig_NodeInterface $body = null,
+        \Twig_Node $body = null,
         \Twig_Node_Expression $file = null,
         \Twig_Node_Expression $group = null,
         \Twig_Node_Expression $priority = null,
         \Twig_Node_Expression $attributes = null,
-        $lineno,
+        $lineno = 0,
         $tag = null
     )
     {

+ 12 - 10
system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php

@@ -10,7 +10,13 @@ namespace Grav\Common\Twig\Node;
 
 class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
 {
-    public function __construct(\Twig_NodeInterface $value, \Twig_NodeInterface $cases, \Twig_NodeInterface $default = null, $lineno, $tag = null)
+    public function __construct(
+        \Twig_Node $value,
+        \Twig_Node $cases,
+        \Twig_Node $default = null,
+        $lineno = 0,
+        $tag = null
+    )
     {
         parent::__construct(array('value' => $value, 'cases' => $cases, 'default' => $default), array(), $lineno, $tag);
     }
@@ -24,20 +30,17 @@ class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
     {
         $compiler
             ->addDebugInfo($this)
-            ->write("switch (")
+            ->write('switch (')
             ->subcompile($this->getNode('value'))
             ->raw(") {\n")
             ->indent();
 
-        foreach ($this->getNode('cases') as $case)
-        {
-            if (!$case->hasNode('body'))
-            {
+        foreach ($this->getNode('cases') as $case) {
+            if (!$case->hasNode('body')) {
                 continue;
             }
 
-            foreach ($case->getNode('values') as $value)
-            {
+            foreach ($case->getNode('values') as $value) {
                 $compiler
                     ->write('case ')
                     ->subcompile($value)
@@ -53,8 +56,7 @@ class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
                 ->write("}\n");
         }
 
-        if ($this->hasNode('default') && $this->getNode('default') !== null)
-        {
+        if ($this->hasNode('default') && $this->getNode('default') !== null) {
             $compiler
                 ->write("default:\n")
                 ->write("{\n")

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

@@ -10,7 +10,12 @@ namespace Grav\Common\Twig\Node;
 
 class TwigNodeTryCatch extends \Twig_Node
 {
-    public function __construct(\Twig_NodeInterface $try, \Twig_NodeInterface $catch = null, $lineno, $tag = null)
+    public function __construct(
+        \Twig_Node $try,
+        \Twig_Node $catch = null,
+        $lineno = 0,
+        $tag = null
+    )
     {
         parent::__construct(array('try' => $try, 'catch' => $catch), array(), $lineno, $tag);
     }

+ 1 - 1
system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php

@@ -27,7 +27,7 @@ class TwigTokenParserScript extends \Twig_TokenParser
      *
      * @param \Twig_Token $token A Twig_Token instance
      *
-     * @return \Twig_NodeInterface A Twig_NodeInterface instance
+     * @return \Twig_Node A Twig_Node instance
      */
     public function parse(\Twig_Token $token)
     {

+ 1 - 1
system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php

@@ -26,7 +26,7 @@ class TwigTokenParserStyle extends \Twig_TokenParser
      *
      * @param \Twig_Token $token A Twig_Token instance
      *
-     * @return \Twig_NodeInterface A Twig_NodeInterface instance
+     * @return \Twig_Node A Twig_Node instance
      */
     public function parse(\Twig_Token $token)
     {

+ 30 - 43
system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php

@@ -37,8 +37,7 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
         $stream->expect(\Twig_Token::BLOCK_END_TYPE);
 
         // There can be some whitespace between the {% switch %} and first {% case %} tag.
-        while ($stream->getCurrent()->getType() == \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) == '')
-        {
+        while ($stream->getCurrent()->getType() === \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') {
             $stream->next();
         }
 
@@ -47,56 +46,45 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
         $expressionParser = $this->parser->getExpressionParser();
 
         $default = null;
-        $cases = array();
+        $cases = [];
         $end = false;
 
-        while (!$end)
-        {
+        while (!$end) {
             $next = $stream->next();
 
-            switch ($next->getValue())
-            {
+            switch ($next->getValue()) {
                 case 'case':
-                    {
-                        $values = array();
-
-                        while (true)
-                        {
-                            $values[] = $expressionParser->parsePrimaryExpression();
-                            // Multiple allowed values?
-                            if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or'))
-                            {
-                                $stream->next();
-                            }
-                            else
-                            {
-                                break;
-                            }
+                    $values = [];
+
+                    while (true) {
+                        $values[] = $expressionParser->parsePrimaryExpression();
+                        // Multiple allowed values?
+                        if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or')) {
+                            $stream->next();
+                        } else {
+                            break;
                         }
-
-                        $stream->expect(\Twig_Token::BLOCK_END_TYPE);
-                        $body = $this->parser->subparse(array($this, 'decideIfFork'));
-                        $cases[] = new \Twig_Node(array(
-                            'values' => new \Twig_Node($values),
-                            'body' => $body
-                        ));
-                        break;
                     }
+
+                    $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+                    $body = $this->parser->subparse(array($this, 'decideIfFork'));
+                    $cases[] = new \Twig_Node([
+                        'values' => new \Twig_Node($values),
+                        'body' => $body
+                    ]);
+                    break;
+
                 case 'default':
-                    {
-                        $stream->expect(\Twig_Token::BLOCK_END_TYPE);
-                        $default = $this->parser->subparse(array($this, 'decideIfEnd'));
-                        break;
-                    }
+                    $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+                    $default = $this->parser->subparse(array($this, 'decideIfEnd'));
+                    break;
+
                 case 'endswitch':
-                    {
-                        $end = true;
-                        break;
-                    }
+                    $end = true;
+                    break;
+
                 default:
-                    {
-                        throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1);
-                    }
+                    throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1);
             }
         }
 
@@ -127,7 +115,6 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
         return $token->test(array('endswitch'));
     }
 
-
     /**
      * {@inheritdoc}
      */

+ 1 - 1
system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php

@@ -28,7 +28,7 @@ class TwigTokenParserTryCatch extends \Twig_TokenParser
      *
      * @param \Twig_Token $token A Twig_Token instance
      *
-     * @return \Twig_NodeInterface A Twig_NodeInterface instance
+     * @return \Twig_Node A Twig_Node instance
      */
     public function parse(\Twig_Token $token)
     {

+ 10 - 7
system/src/Grav/Common/Twig/Twig.php

@@ -113,7 +113,10 @@ class Twig
                 $params['cache'] = new \Twig_Cache_Filesystem($cachePath, \Twig_Cache_Filesystem::FORCE_BYTECODE_INVALIDATION);
             }
 
-            if (!empty($this->autoescape)) {
+            if (!$config->get('system.strict_mode.twig_compat', true)) {
+                // Force autoescape on for all files if in strict mode.
+                $params['autoescape'] = true;
+            } elseif (!empty($this->autoescape)) {
                 $params['autoescape'] = $this->autoescape;
             }
 
@@ -122,10 +125,10 @@ class Twig
             if ($config->get('system.twig.undefined_functions')) {
                 $this->twig->registerUndefinedFunctionCallback(function ($name) {
                     if (function_exists($name)) {
-                        return new \Twig_Function_Function($name);
+                        return new \Twig_SimpleFunction($name, $name);
                     }
 
-                    return new \Twig_Function_Function(function () {
+                    return new \Twig_SimpleFunction($name, function () {
                     });
                 });
             }
@@ -133,10 +136,10 @@ class Twig
             if ($config->get('system.twig.undefined_filters')) {
                 $this->twig->registerUndefinedFilterCallback(function ($name) {
                     if (function_exists($name)) {
-                        return new \Twig_Filter_Function($name);
+                        return new \Twig_SimpleFilter($name, $name);
                     }
 
-                    return new \Twig_Filter_Function(function () {
+                    return new \Twig_SimpleFilter($name, function () {
                     });
                 });
             }
@@ -145,7 +148,7 @@ class Twig
 
             // set default date format if set in config
             if ($config->get('system.pages.dateformat.long')) {
-                $this->twig->getExtension('core')->setDateFormat($config->get('system.pages.dateformat.long'));
+                $this->twig->getExtension('Twig_Extension_Core')->setDateFormat($config->get('system.pages.dateformat.long'));
             }
             // enable the debug extension if required
             if ($config->get('system.twig.debug')) {
@@ -159,7 +162,7 @@ class Twig
             $pages = $this->grav['pages'];
 
             // Set some standard variables for twig
-            $this->twig_vars = $this->twig_vars + [
+            $this->twig_vars += [
                     'config'            => $config,
                     'system'            => $config->get('system'),
                     'theme'             => $config->get('theme'),

+ 81 - 28
system/src/Grav/Common/Twig/TwigExtension.php

@@ -18,11 +18,11 @@ use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch;
 use Grav\Common\Twig\TokenParser\TwigTokenParserMarkdown;
 use Grav\Common\User\User;
 use Grav\Common\Utils;
+use Grav\Common\Yaml;
 use Grav\Common\Markdown\Parsedown;
 use Grav\Common\Markdown\ParsedownExtra;
 use Grav\Common\Helpers\Base32;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
-use Symfony\Component\Yaml\Yaml;
 
 class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface
 {
@@ -72,7 +72,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
             new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']),
             new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']),
             new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
-            new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction']),
+            new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]),
             new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
             new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']),
             new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']),
@@ -88,9 +88,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
             new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']),
             new \Twig_SimpleFilter('sort_by_key', [$this, 'sortByKeyFilter']),
             new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
-            new \Twig_SimpleFilter('t', [$this, 'translate']),
-            new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']),
-            new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
             new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
             new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
             new \Twig_SimpleFilter('json_decode', [$this, 'jsonDecodeFilter']),
@@ -100,6 +97,18 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
             new \Twig_SimpleFilter('print_r', 'print_r'),
             new \Twig_SimpleFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
             new \Twig_SimpleFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
+
+            // Translations
+            new \Twig_SimpleFilter('t', [$this, 'translate']),
+            new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']),
+            new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
+
+            // Casting values
+            new \Twig_SimpleFilter('string', [$this, 'stringFilter']),
+            new \Twig_SimpleFilter('int', [$this, 'intFilter'], ['is_safe' => true]),
+            new \Twig_SimpleFilter('bool', [$this, 'boolFilter']),
+            new \Twig_SimpleFilter('float', [$this, 'floatFilter'], ['is_safe' => true]),
+            new \Twig_SimpleFilter('array', [$this, 'arrayFilter']),
         ];
     }
 
@@ -111,7 +120,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
     public function getFunctions()
     {
         return [
-            new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
+            new \Twig_SimpleFunction('array', [$this, 'arrayFilter']),
             new \Twig_SimpleFunction('array_key_value', [$this, 'arrayKeyValueFunc']),
             new \Twig_SimpleFunction('array_key_exists', 'array_key_exists'),
             new \Twig_SimpleFunction('array_unique', 'array_unique'),
@@ -132,9 +141,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
             new \Twig_SimpleFunction('regex_replace', [$this, 'regexReplace']),
             new \Twig_SimpleFunction('regex_filter', [$this, 'regexFilter']),
             new \Twig_SimpleFunction('string', [$this, 'stringFunc']),
-            new \Twig_simpleFunction('t', [$this, 'translate']),
-            new \Twig_simpleFunction('tl', [$this, 'translateLanguage']),
-            new \Twig_simpleFunction('ta', [$this, 'translateArray']),
             new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
             new \Twig_SimpleFunction('json_decode', [$this, 'jsonDecodeFilter']),
             new \Twig_SimpleFunction('get_cookie', [$this, 'getCookie']),
@@ -151,6 +157,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
             new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
             new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFilter']),
 
+            // Translations
+            new \Twig_simpleFunction('t', [$this, 'translate']),
+            new \Twig_simpleFunction('tl', [$this, 'translateLanguage']),
+            new \Twig_simpleFunction('ta', [$this, 'translateArray']),
         ];
     }
 
@@ -617,6 +627,62 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
         return ltrim($value, $chars);
     }
 
+    /**
+     * Casts input to string.
+     *
+     * @param mixed $input
+     * @return string
+     */
+    public function stringFilter($input)
+    {
+        return (string) $input;
+    }
+
+
+    /**
+     * Casts input to int.
+     *
+     * @param mixed $input
+     * @return int
+     */
+    public function intFilter($input)
+    {
+        return (int) $input;
+    }
+
+    /**
+     * Casts input to bool.
+     *
+     * @param mixed $input
+     * @return bool
+     */
+    public function boolFilter($input)
+    {
+        return (bool) $input;
+    }
+
+    /**
+     * Casts input to float.
+     *
+     * @param mixed $input
+     * @return float
+     */
+    public function floatFilter($input)
+    {
+        return (float) $input;
+    }
+
+    /**
+     * Casts input to array.
+     *
+     * @param mixed $input
+     * @return array
+     */
+    public function arrayFilter($input)
+    {
+        return (array) $input;
+    }
+
     /**
      * @return mixed
      */
@@ -693,7 +759,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
 
         $template = $env->createTemplate($twig);
         return $template->render($context);
-;
     }
 
     /**
@@ -748,7 +813,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
      * Output a Gist
      *
      * @param  string $id
-     * @param  string $file
+     * @param  string|bool $file
      *
      * @return string
      */
@@ -788,19 +853,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
         return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
     }
 
-
-    /**
-     * Cast a value to array
-     *
-     * @param $value
-     *
-     * @return array
-     */
-    public function arrayFunc($value)
-    {
-        return (array)$value;
-    }
-
     /**
      * Workaround for twig associative array initialization
      * Returns a key => val array
@@ -976,7 +1028,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
     public function redirectFunc($url, $statusCode = 303)
     {
         header('Location: ' . $url, true, $statusCode);
-        die();
+        exit();
     }
 
     /**
@@ -1060,7 +1112,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
 
         if (file_exists($filepath)) {
             return file_get_contents($filepath);
-    }
+        }
 
         return false;
     }
@@ -1245,11 +1297,12 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
      * Dump/Encode data into YAML format
      *
      * @param $data
+     * @param $inline integer number of levels of inline syntax
      * @return mixed
      */
-    public function yamlEncodeFilter($data)
+    public function yamlEncodeFilter($data, $inline = 10)
     {
-        return Yaml::dump($data, 10);
+        return Yaml::dump($data, $inline);
     }
 
     /**

+ 41 - 30
system/src/Grav/Common/Uri.php

@@ -11,6 +11,7 @@ namespace Grav\Common;
 use Grav\Common\Config\Config;
 use Grav\Common\Language\Language;
 use Grav\Common\Page\Page;
+use Grav\Common\Page\Pages;
 use Grav\Framework\Route\RouteFactory;
 use Grav\Framework\Uri\UriFactory;
 use Grav\Framework\Uri\UriPartsFilter;
@@ -156,12 +157,6 @@ class Uri
             $uri = preg_replace('|^' . preg_quote($setup_base, '|') . '|', '', $uri);
         }
 
-        // If configured to, redirect trailing slash URI's with a 302 redirect
-        $redirect = str_replace($this->root, '', rtrim($uri, '/'));
-        if ($redirect && $uri !== '/' && $redirect !== $this->base() && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($uri, '/')) {
-            $grav->redirect($redirect, 302);
-        }
-
         // process params
         $uri = $this->processParams($uri, $config->get('system.param_sep'));
 
@@ -206,9 +201,9 @@ class Uri
         }
 
         // Set some Grav stuff
-        $grav['base_url_absolute'] = $grav['config']->get('system.custom_base_url') ?: $this->rootUrl(true);
+        $grav['base_url_absolute'] = $config->get('system.custom_base_url') ?: $this->rootUrl(true);
         $grav['base_url_relative'] = $this->rootUrl(false);
-        $grav['base_url'] = $grav['config']->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative'];
+        $grav['base_url'] = $config->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative'];
 
         RouteFactory::setRoot($this->root_path);
         RouteFactory::setLanguage($language->getLanguageURLPrefix());
@@ -376,6 +371,17 @@ class Uri
         return $this->extension;
     }
 
+    public function method()
+    {
+        $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
+
+        if ($method === 'POST' && isset($_SERVER['X-HTTP-METHOD-OVERRIDE'])) {
+            $method = strtoupper($_SERVER['X-HTTP-METHOD-OVERRIDE']);
+        }
+
+        return $method;
+    }
+
     /**
      * Return the scheme of the URI
      *
@@ -481,11 +487,9 @@ class Uri
     {
         if ($include_root) {
             return $this->uri;
-        } else {
-            $uri = str_replace($this->root_path, '', $this->uri);
-            return $uri;
         }
 
+        return str_replace($this->root_path, '', $this->uri);
     }
 
     /**
@@ -508,16 +512,10 @@ class Uri
     {
         $grav = Grav::instance();
 
-        // Link processing should prepend language
-        $language = $grav['language'];
-        $language_append = '';
-        if ($language->enabled()) {
-            $language_append = $language->getLanguageURLPrefix();
-        }
-
-        $base = $grav['base_url_relative'];
+        /** @var Pages $pages */
+        $pages = $grav['pages'];
 
-        return rtrim($base . $grav['pages']->base(), '/') . $language_append;
+        return $pages->baseUrl(null, false);
     }
 
     /**
@@ -633,10 +631,9 @@ class Uri
         }
 
         return $ip;
-
     }
-    /**
 
+    /**
      * Returns current Uri.
      *
      * @return \Grav\Framework\Uri\Uri
@@ -883,7 +880,26 @@ class Uri
     public static function parseUrl($url)
     {
         $grav = Grav::instance();
-        $parts = parse_url($url);
+
+        $encodedUrl = preg_replace_callback(
+            '%[^:/@?&=#]+%usD',
+            function ($matches) { return rawurlencode($matches[0]); },
+            $url
+        );
+
+        $parts = parse_url($encodedUrl);
+
+        if (false === $parts) {
+            return false;
+        }
+
+        foreach($parts as $name => $value) {
+            $parts[$name] = rawurldecode($value);
+        }
+
+        if (!isset($parts['path'])) {
+            $parts['path'] = '';
+        }
 
         list($stripped_path, $params) = static::extractParams($parts['path'], $grav['config']->get('system.param_sep'));
 
@@ -1262,7 +1278,7 @@ class Uri
     {
         if (!$this->post) {
             $content_type = $this->getContentType();
-            if ($content_type == 'application/json') {
+            if ($content_type === 'application/json') {
                 $json = file_get_contents('php://input');
                 $this->post = json_decode($json, true);
             } elseif (!empty($_POST)) {
@@ -1270,7 +1286,7 @@ class Uri
             }
         }
 
-        if ($this->post && !is_null($element)) {
+        if ($this->post && null !== $element) {
             $item = Utils::getDotNotation($this->post, $element);
             if ($filter_type) {
                 $item = filter_var($item, $filter_type);
@@ -1320,11 +1336,6 @@ class Uri
         $scriptPath = str_replace('\\', '/', $_SERVER['PHP_SELF']);
         $rootPath = str_replace(' ', '%20', rtrim(substr($scriptPath, 0, strpos($scriptPath, 'index.php')), '/'));
 
-        // check if userdir in the path and workaround PHP bug with PHP_SELF
-        if (strpos($this->uri, '/~') !== false && strpos($scriptPath, '/~') === false) {
-            $rootPath = substr($this->uri, 0, strpos($this->uri, '/', 1)) . $rootPath;
-        }
-
         return $rootPath;
     }
 

+ 28 - 62
system/src/Grav/Common/Utils.php

@@ -45,8 +45,20 @@ abstract class Utils
             /** @var UniformResourceLocator $locator */
             $locator = Grav::instance()['locator'];
 
-            // Get relative path to the resource (or false if not found).
-            $resource = $locator->findResource($input, false);
+            $parts = Uri::parseUrl($input);
+
+            if ($parts) {
+                $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
+
+                if (isset($parts['query'])) {
+                    $resource = $resource . '?' . $parts['query'];
+                }
+            } else {
+                // Not a valid URL (can still be a stream).
+                $resource = $locator->findResource($input, false);
+            }
+
+
         } else {
             $resource = $input;
         }
@@ -262,7 +274,7 @@ abstract class Utils
         // is $break present between $limit and the end of the string?
         if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
             if ($breakpoint < mb_strlen($string) - 1) {
-                $string = mb_substr($string, 0, $breakpoint) . $break;
+                $string = mb_substr($string, 0, $breakpoint) . $pad;
             }
         } else {
             $string = mb_substr($string, 0, $limit) . $pad;
@@ -705,11 +717,11 @@ abstract class Utils
      * with reverse proxy setups.
      *
      * @param string $action
-     * @param bool   $plusOneTick if true, generates the token for the next tick (the next 12 hours)
+     * @param bool   $previousTick if true, generates the token for the previous tick (the previous 12 hours)
      *
      * @return string the nonce string
      */
-    private static function generateNonceString($action, $plusOneTick = false)
+    private static function generateNonceString($action, $previousTick = false)
     {
         $username = '';
         if (isset(Grav::instance()['user'])) {
@@ -720,29 +732,8 @@ abstract class Utils
         $token = session_id();
         $i = self::nonceTick();
 
-        if ($plusOneTick) {
-            $i++;
-        }
-
-        return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
-    }
-
-    //Added in version 1.0.8 to ensure that existing nonces are not broken.
-    private static function generateNonceStringOldStyle($action, $plusOneTick = false)
-    {
-        if (isset(Grav::instance()['user'])) {
-            $user = Grav::instance()['user'];
-            $username = $user->username;
-            if (isset($_SERVER['REMOTE_ADDR'])) {
-                $username .= $_SERVER['REMOTE_ADDR'];
-            }
-        } else {
-            $username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
-        }
-        $token = session_id();
-        $i = self::nonceTick();
-        if ($plusOneTick) {
-            $i++;
+        if ($previousTick) {
+            $i--;
         }
 
         return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
@@ -768,33 +759,20 @@ abstract class Utils
      * action is the same for 12 hours.
      *
      * @param string $action      the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
-     * @param bool   $plusOneTick if true, generates the token for the next tick (the next 12 hours)
+     * @param bool   $previousTick if true, generates the token for the previous tick (the previous 12 hours)
      *
      * @return string the nonce
      */
-    public static function getNonce($action, $plusOneTick = false)
-    {
-        // Don't regenerate this again if not needed
-        if (isset(static::$nonces[$action])) {
-            return static::$nonces[$action];
-        }
-        $nonce = md5(self::generateNonceString($action, $plusOneTick));
-        static::$nonces[$action] = $nonce;
-
-        return static::$nonces[$action];
-    }
-
-    //Added in version 1.0.8 to ensure that existing nonces are not broken.
-    public static function getNonceOldStyle($action, $plusOneTick = false)
+    public static function getNonce($action, $previousTick = false)
     {
         // Don't regenerate this again if not needed
-        if (isset(static::$nonces[$action])) {
-            return static::$nonces[$action];
+        if (isset(static::$nonces[$action][$previousTick])) {
+            return static::$nonces[$action][$previousTick];
         }
-        $nonce = md5(self::generateNonceStringOldStyle($action, $plusOneTick));
-        static::$nonces[$action] = $nonce;
+        $nonce = md5(self::generateNonceString($action, $previousTick));
+        static::$nonces[$action][$previousTick] = $nonce;
 
-        return static::$nonces[$action];
+        return static::$nonces[$action][$previousTick];
     }
 
     /**
@@ -818,20 +796,8 @@ abstract class Utils
         }
 
         //Nonce generated 12-24 hours ago
-        $plusOneTick = true;
-        if ($nonce === self::getNonce($action, $plusOneTick)) {
-            return true;
-        }
-
-        //Added in version 1.0.8 to ensure that existing nonces are not broken.
-        //Nonce generated 0-12 hours ago
-        if ($nonce === self::getNonceOldStyle($action)) {
-            return true;
-        }
-
-        //Nonce generated 12-24 hours ago
-        $plusOneTick = true;
-        if ($nonce === self::getNonceOldStyle($action, $plusOneTick)) {
+        $previousTick = true;
+        if ($nonce === self::getNonce($action, $previousTick)) {
             return true;
         }
 

+ 47 - 0
system/src/Grav/Common/Yaml.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * @package    Grav.Common
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common;
+
+use Grav\Framework\File\Formatter\YamlFormatter;
+
+abstract class Yaml
+{
+    /** @var YamlFormatter */
+    private static $yaml;
+
+    public static function parse($data)
+    {
+        if (null === static::$yaml) {
+            static::init();
+        }
+
+        return static::$yaml->decode($data);
+    }
+
+    public static function dump($data, $inline = null, $indent = null)
+    {
+        if (null === static::$yaml) {
+            static::init();
+        }
+
+        return static::$yaml->encode($data, $inline, $indent);
+    }
+
+    private static function init()
+    {
+        $config = [
+            'inline' => 5,
+            'indent' => 2,
+            'native' => true,
+            'compat' => true
+        ];
+
+        static::$yaml = new YamlFormatter($config);
+    }
+}

+ 25 - 13
system/src/Grav/Console/Cli/InstallCommand.php

@@ -9,9 +9,9 @@
 namespace Grav\Console\Cli;
 
 use Grav\Console\ConsoleCommand;
+use RocketTheme\Toolbox\File\YamlFile;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Yaml\Yaml;
 
 class InstallCommand extends ConsoleCommand
 {
@@ -71,9 +71,9 @@ class InstallCommand extends ConsoleCommand
 
         // Look for dependencies file in ROOT and USER dir
         if (file_exists($this->user_path . $dependencies_file)) {
-            $this->config = Yaml::parse(file_get_contents($this->user_path . $dependencies_file));
+            $file = YamlFile::instance($this->user_path . $dependencies_file);
         } elseif (file_exists($this->destination . $dependencies_file)) {
-            $this->config = Yaml::parse(file_get_contents($this->destination . $dependencies_file));
+            $file = YamlFile::instance($this->destination . $dependencies_file);
         } else {
             $this->output->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
             if ($this->input->getArgument('destination')) {
@@ -85,6 +85,9 @@ class InstallCommand extends ConsoleCommand
             return;
         }
 
+        $this->config = $file->content();
+        $file->free();
+
         // If yaml config, process
         if ($this->config) {
             if (!$this->input->getOption('symlink')) {
@@ -153,23 +156,32 @@ class InstallCommand extends ConsoleCommand
 
         exec('cd ' . $this->destination);
         foreach ($this->config['links'] as $repo => $data) {
-            $from = $this->local_config[$data['scm'] . '_repos'] . $data['src'];
+            $repos = (array) $this->local_config[$data['scm'] . '_repos'];
+            $from = false;
             $to = $this->destination . $data['path'];
 
-            if (file_exists($from)) {
-                if (!file_exists($to)) {
-                    symlink($from, $to);
-                    $this->output->writeln('<green>SUCCESS</green> symlinked <magenta>' . $data['src'] . '</magenta> -> <cyan>' . $data['path'] . '</cyan>');
-                    $this->output->writeln('');
-                } else {
-                    $this->output->writeln('<red>destination: ' . $to . ' already exists, skipping...</red>');
-                    $this->output->writeln('');
+            foreach ($repos as $repo) {
+                $path = $repo . $data['src'];
+                if (file_exists($path)) {
+                    $from = $path;
+                    continue;
                 }
-            } else {
+            }
+
+            if (!$from) {
                 $this->output->writeln('<red>source: ' . $from . ' does not exists, skipping...</red>');
                 $this->output->writeln('');
             }
 
+            if (!file_exists($to)) {
+                symlink($from, $to);
+                $this->output->writeln('<green>SUCCESS</green> symlinked <magenta>' . $data['src'] . '</magenta> -> <cyan>' . $data['path'] . '</cyan>');
+                $this->output->writeln('');
+            } else {
+                $this->output->writeln('<red>destination: ' . $to . ' already exists, skipping...</red>');
+                $this->output->writeln('');
+            }
+
         }
     }
 }

+ 4 - 2
system/src/Grav/Console/ConsoleTrait.php

@@ -12,11 +12,11 @@ use Grav\Common\Grav;
 use Grav\Common\Composer;
 use Grav\Common\GravTrait;
 use Grav\Console\Cli\ClearCacheCommand;
+use RocketTheme\Toolbox\File\YamlFile;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Yaml\Yaml;
 
 trait ConsoleTrait
 {
@@ -123,7 +123,9 @@ trait ConsoleTrait
         $local_config_file = $home_folder . '/.grav/config';
 
         if (file_exists($local_config_file)) {
-            $this->local_config = Yaml::parse(file_get_contents($local_config_file));
+            $file = YamlFile::instance($local_config_file);
+            $this->local_config = $file->content();
+            $file->free();
             return $local_config_file;
         }
 

+ 9 - 6
system/src/Grav/Console/Gpm/InstallCommand.php

@@ -444,18 +444,21 @@ class InstallCommand extends ConsoleCommand
     {
         $matches = $this->getGitRegexMatches($package);
 
-        foreach ($this->local_config as $path) {
+        foreach ($this->local_config as $paths) {
             if (Utils::endsWith($matches[2], '.git')) {
                 $repo_dir = preg_replace('/\.git$/', '', $matches[2]);
             } else {
                 $repo_dir = $matches[2];
             }
-
-            $from = rtrim($path, '/') . '/' . $repo_dir;
-
-            if (file_exists($from)) {
-                return $from;
+            
+            $paths = (array) $paths;
+            foreach ($paths as $repo) {
+                $path = rtrim($repo, '/') . '/' . $repo_dir;
+                if (file_exists($path)) {
+                    return $path;
+                }
             }
+
         }
 
         return false;

+ 5 - 2
system/src/Grav/Console/Gpm/VersionCommand.php

@@ -11,9 +11,9 @@ namespace Grav\Console\Gpm;
 use Grav\Common\GPM\GPM;
 use Grav\Common\GPM\Upgrader;
 use Grav\Console\ConsoleCommand;
+use RocketTheme\Toolbox\File\YamlFile;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Yaml\Yaml;
 
 class VersionCommand extends ConsoleCommand
 {
@@ -84,7 +84,10 @@ class VersionCommand extends ConsoleCommand
                     }
                 }
 
-                $package_yaml = Yaml::parse(file_get_contents($blueprints_path));
+                $file = YamlFile::instance($blueprints_path);
+                $package_yaml = $file->content();
+                $file->free();
+
                 $version = $package_yaml['version'];
 
                 if (!$version) {

+ 18 - 9
system/src/Grav/Framework/Cache/CacheTrait.php

@@ -16,21 +16,18 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
  */
 trait CacheTrait
 {
-    /**
-     * @var string
-     */
+    /** @var string */
     private $namespace = '';
 
-    /**
-     * @var int|null
-     */
+    /** @var int|null */
     private $defaultLifetime = null;
 
-    /**
-     * @var \stdClass
-     */
+    /** @var \stdClass */
     private $miss;
 
+    /** @var bool */
+    private $validation = true;
+
     /**
      * Always call from constructor.
      *
@@ -45,6 +42,14 @@ trait CacheTrait
         $this->miss = new \stdClass;
     }
 
+    /**
+     * @param $validation
+     */
+    public function setValidation($validation)
+    {
+        $this->validation = (bool) $validation;
+    }
+
     /**
      * @return string
      */
@@ -307,6 +312,10 @@ trait CacheTrait
      */
     protected function validateKeys($keys)
     {
+        if (!$this->validation) {
+            return;
+        }
+
         foreach ($keys as $key) {
             $this->validateKey($key);
         }

+ 0 - 10
system/src/Grav/Framework/Collection/ArrayCollection.php

@@ -24,11 +24,6 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
      */
     public function reverse()
     {
-        // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
-        if (!method_exists($this, 'createFrom')) {
-            return new static(array_reverse($this->toArray()));
-        }
-
         return $this->createFrom(array_reverse($this->toArray()));
     }
 
@@ -42,11 +37,6 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
         $keys = $this->getKeys();
         shuffle($keys);
 
-        // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
-        if (!method_exists($this, 'createFrom')) {
-            return new static(array_replace(array_flip($keys), $this->toArray()));
-        }
-
         return $this->createFrom(array_replace(array_flip($keys), $this->toArray()));
     }
 

+ 30 - 4
system/src/Grav/Framework/ContentBlock/ContentBlock.php

@@ -27,6 +27,7 @@ class ContentBlock implements ContentBlockInterface
     protected $tokenTemplate = '@@BLOCK-%s@@';
     protected $content = '';
     protected $blocks = [];
+    protected $checksum;
 
     /**
      * @param string $id
@@ -40,6 +41,7 @@ class ContentBlock implements ContentBlockInterface
     /**
      * @param array $serialized
      * @return ContentBlockInterface
+     * @throws \InvalidArgumentException
      */
     public static function fromArray(array $serialized)
     {
@@ -48,14 +50,14 @@ class ContentBlock implements ContentBlockInterface
             $id = isset($serialized['id']) ? $serialized['id'] : null;
 
             if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) {
-                throw new \RuntimeException('Bad data');
+                throw new \InvalidArgumentException('Bad data');
             }
 
             /** @var ContentBlockInterface $instance */
             $instance = new $type($id);
             $instance->build($serialized);
         } catch (\Exception $e) {
-            throw new \RuntimeException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
+            throw new \InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
         }
 
         return $instance;
@@ -104,9 +106,13 @@ class ContentBlock implements ContentBlockInterface
         $array = [
             '_type' => get_class($this),
             '_version' => $this->version,
-            'id' => $this->id,
+            'id' => $this->id
         ];
 
+        if ($this->checksum) {
+            $array['checksum'] = $this->checksum;
+        }
+
         if ($this->content) {
             $array['content'] = $this->content;
         }
@@ -158,6 +164,7 @@ class ContentBlock implements ContentBlockInterface
         $this->checkVersion($serialized);
 
         $this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
+        $this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null;
 
         if (isset($serialized['content'])) {
             $this->setContent($serialized['content']);
@@ -169,6 +176,25 @@ class ContentBlock implements ContentBlockInterface
         }
     }
 
+    /**
+     * @param string $checksum
+     * @return $this
+     */
+    public function setChecksum($checksum)
+    {
+        $this->checksum = $checksum;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getChecksum()
+    {
+        return $this->checksum;
+    }
+
     /**
      * @param string $content
      * @return $this
@@ -222,7 +248,7 @@ class ContentBlock implements ContentBlockInterface
      */
     protected function checkVersion(array $serialized)
     {
-        $version = isset($serialized['_version']) ? (string) $serialized['_version'] : '1';
+        $version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1;
         if ($version !== $this->version) {
             throw new \RuntimeException(sprintf('Unsupported version %s', $version));
         }

+ 11 - 0
system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php

@@ -61,6 +61,17 @@ interface ContentBlockInterface extends \Serializable
      */
     public function build(array $serialized);
 
+    /**
+     * @param string $checksum
+     * @return $this
+     */
+    public function setChecksum($checksum);
+
+    /**
+     * @return string
+     */
+    public function getChecksum();
+
     /**
      * @param string $content
      * @return $this

+ 1 - 0
system/src/Grav/Framework/ContentBlock/HtmlBlock.php

@@ -15,6 +15,7 @@ namespace Grav\Framework\ContentBlock;
  */
 class HtmlBlock extends ContentBlock implements HtmlBlockInterface
 {
+    protected $version = 1;
     protected $frameworks = [];
     protected $styles = [];
     protected $scripts = [];

+ 44 - 0
system/src/Grav/Framework/File/Formatter/FormatterInterface.php

@@ -0,0 +1,44 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+interface FormatterInterface
+{
+    /**
+     * Get default file extension from current formatter (with dot).
+     *
+     * Default file extension is the first defined extension.
+     *
+     * @return string File extension (can be empty).
+     */
+    public function getDefaultFileExtension();
+
+    /**
+     * Get file extensions supported by current formatter (with dot).
+     *
+     * @return string[]
+     */
+    public function getSupportedFileExtensions();
+
+    /**
+     * Encode data into a string.
+     *
+     * @param array $data
+     * @return string
+     */
+    public function encode($data);
+
+    /**
+     * Decode a string into data.
+     *
+     * @param string $data
+     * @return array
+     */
+    public function decode($data);
+}

+ 83 - 0
system/src/Grav/Framework/File/Formatter/IniFormatter.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+class IniFormatter implements FormatterInterface
+{
+    /** @var array */
+    private $config;
+
+    /**
+     * IniFormatter constructor.
+     * @param array $config
+     */
+    public function __construct(array $config = [])
+    {
+        $this->config = $config + [
+                'file_extension' => '.ini'
+            ];
+    }
+
+    /**
+     * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
+     */
+    public function getFileExtension()
+    {
+        return $this->getDefaultFileExtension();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefaultFileExtension()
+    {
+        $extensions = $this->getSupportedFileExtensions();
+
+        return (string) reset($extensions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedFileExtensions()
+    {
+        return (array) $this->config['file_extension'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data)
+    {
+        $string = '';
+        foreach ($data as $key => $value) {
+            $string .= $key . '="' .  preg_replace(
+                    ['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
+                    ['\"',  '\\\\', '\t',   '\n',   '\r'],
+                    $value
+                ) . "\"\n";
+        }
+
+        return $string;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data)
+    {
+        $decoded = @parse_ini_string($data);
+
+        if ($decoded === false) {
+            throw new \RuntimeException('Decoding INI failed');
+        }
+
+        return $decoded;
+    }
+}

+ 78 - 0
system/src/Grav/Framework/File/Formatter/JsonFormatter.php

@@ -0,0 +1,78 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+class JsonFormatter implements FormatterInterface
+{
+    /** @var array */
+    private $config;
+
+    public function __construct(array $config = [])
+    {
+        $this->config = $config + [
+            'file_extension' => '.json',
+            'encode_options' => 0,
+            'decode_assoc' => true
+        ];
+    }
+
+    /**
+     * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
+     */
+    public function getFileExtension()
+    {
+        return $this->getDefaultFileExtension();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefaultFileExtension()
+    {
+        $extensions = $this->getSupportedFileExtensions();
+
+        return (string) reset($extensions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedFileExtensions()
+    {
+        return (array) $this->config['file_extension'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data)
+    {
+        $encoded = @json_encode($data, $this->config['encode_options']);
+
+        if ($encoded === false) {
+            throw new \RuntimeException('Encoding JSON failed');
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data)
+    {
+        $decoded = @json_decode($data, $this->config['decode_assoc']);
+
+        if ($decoded === false) {
+            throw new \RuntimeException('Decoding JSON failed');
+        }
+
+        return $decoded;
+    }
+}

+ 116 - 0
system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+class MarkdownFormatter implements FormatterInterface
+{
+    /** @var array */
+    private $config;
+    /** @var FormatterInterface */
+    private $headerFormatter;
+
+    public function __construct(array $config = [], FormatterInterface $headerFormatter = null)
+    {
+        $this->config = $config + [
+            'file_extension' => '.md',
+            'header' => 'header',
+            'body' => 'markdown',
+            'raw' => 'frontmatter',
+            'yaml' => ['inline' => 20]
+        ];
+
+        $this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']);
+    }
+
+    /**
+     * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
+     */
+    public function getFileExtension()
+    {
+        return $this->getDefaultFileExtension();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefaultFileExtension()
+    {
+        $extensions = $this->getSupportedFileExtensions();
+
+        return (string) reset($extensions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedFileExtensions()
+    {
+        return (array) $this->config['file_extension'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data)
+    {
+        $headerVar = $this->config['header'];
+        $bodyVar = $this->config['body'];
+
+        $header = isset($data[$headerVar]) ? (array) $data[$headerVar] : [];
+        $body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : '';
+
+        // Create Markdown file with YAML header.
+        $encoded = '';
+        if ($header) {
+            $encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n";
+        }
+        $encoded .= $body;
+
+        // Normalize line endings to Unix style.
+        $encoded = preg_replace("/(\r\n|\r)/", "\n", $encoded);
+
+        return $encoded;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data)
+    {
+        $headerVar = $this->config['header'];
+        $bodyVar = $this->config['body'];
+        $rawVar = $this->config['raw'];
+
+        $content = [
+            $headerVar => [],
+            $bodyVar => ''
+        ];
+
+        $headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis";
+
+        // Normalize line endings to Unix style.
+        $data = preg_replace("/(\r\n|\r)/", "\n", $data);
+
+        // Parse header.
+        preg_match($headerRegex, ltrim($data), $matches);
+        if(empty($matches)) {
+            $content[$bodyVar] = $data;
+        } else {
+            // Normalize frontmatter.
+            $frontmatter = preg_replace("/\n\t/", "\n    ", $matches[1]);
+            if ($rawVar) {
+                $content[$rawVar] = $frontmatter;
+            }
+            $content[$headerVar] = $this->headerFormatter->decode($frontmatter);
+            $content[$bodyVar] = $matches[2];
+        }
+
+        return $content;
+    }
+}

+ 96 - 0
system/src/Grav/Framework/File/Formatter/SerializeFormatter.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+class SerializeFormatter implements FormatterInterface
+{
+    /** @var array */
+    private $config;
+
+    /**
+     * IniFormatter constructor.
+     * @param array $config
+     */
+    public function __construct(array $config = [])
+    {
+        $this->config = $config + [
+                'file_extension' => '.ser'
+            ];
+    }
+
+    /**
+     * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
+     */
+    public function getFileExtension()
+    {
+        return $this->getDefaultFileExtension();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefaultFileExtension()
+    {
+        $extensions = $this->getSupportedFileExtensions();
+
+        return (string) reset($extensions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedFileExtensions()
+    {
+        return (array) $this->config['file_extension'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data)
+    {
+        return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r']));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data)
+    {
+        $decoded = @unserialize($data);
+
+        if ($decoded === false) {
+            throw new \RuntimeException('Decoding serialized data failed');
+        }
+
+        return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]);
+    }
+
+    /**
+     * Preserve new lines, recursive function.
+     *
+     * @param mixed $data
+     * @param array $search
+     * @param array $replace
+     * @return mixed
+     */
+    protected function preserveLines($data, $search, $replace)
+    {
+        if (is_string($data)) {
+            $data = str_replace($search, $replace, $data);
+        } elseif (is_array($data)) {
+            foreach ($data as &$value) {
+                $value = $this->preserveLines($value, $search, $replace);
+            }
+            unset($value);
+        }
+
+        return $data;
+    }
+}

+ 103 - 0
system/src/Grav/Framework/File/Formatter/YamlFormatter.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * @package    Grav\Framework\File\Formatter
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\File\Formatter;
+
+use Symfony\Component\Yaml\Exception\DumpException;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Yaml as YamlParser;
+use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYamlParser;
+
+class YamlFormatter implements FormatterInterface
+{
+    /** @var array */
+    private $config;
+
+    public function __construct(array $config = [])
+    {
+        $this->config = $config + [
+            'file_extension' => '.yaml',
+            'inline' => 5,
+            'indent' => 2,
+            'native' => true,
+            'compat' => true
+        ];
+    }
+
+    /**
+     * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
+     */
+    public function getFileExtension()
+    {
+        return $this->getDefaultFileExtension();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefaultFileExtension()
+    {
+        $extensions = $this->getSupportedFileExtensions();
+
+        return (string) reset($extensions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedFileExtensions()
+    {
+        return (array) $this->config['file_extension'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data, $inline = null, $indent = null)
+    {
+        try {
+            return (string) YamlParser::dump(
+                $data,
+                $inline ? (int) $inline : $this->config['inline'],
+                $indent ? (int) $indent : $this->config['indent'],
+                YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE
+            );
+        } catch (DumpException $e) {
+            throw new \RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data)
+    {
+        // Try native PECL YAML PHP extension first if available.
+        if ($this->config['native'] && function_exists('yaml_parse')) {
+            // Safely decode YAML.
+            $saved = @ini_get('yaml.decode_php');
+            @ini_set('yaml.decode_php', 0);
+            $decoded = @yaml_parse($data);
+            @ini_set('yaml.decode_php', $saved);
+
+            if ($decoded !== false) {
+                return (array) $decoded;
+            }
+        }
+
+        try {
+            return (array) YamlParser::parse($data);
+        } catch (ParseException $e) {
+            if ($this->config['compat']) {
+                return (array) FallbackYamlParser::parse($data);
+            }
+
+            throw new \RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e);
+        }
+    }
+}

+ 1 - 11
system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php

@@ -32,11 +32,6 @@ trait ObjectCollectionTrait
             $list[$key] = is_object($value) ? clone $value : $value;
         }
 
-        // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
-        if (!method_exists($this, 'createFrom')) {
-            return new static($list);
-        }
-
         return $this->createFrom($list);
     }
 
@@ -170,12 +165,7 @@ trait ObjectCollectionTrait
     {
         $collections = [];
         foreach ($this->group($property) as $id => $elements) {
-            // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
-            if (!method_exists($this, 'createFrom')) {
-                $collection = new static($elements);
-            } else {
-                $collection = $this->createFrom($elements);
-            }
+            $collection = $this->createFrom($elements);
 
             $collections[$id] = $collection;
         }

+ 25 - 4
system/src/Grav/Framework/Object/Base/ObjectTrait.php

@@ -15,7 +15,7 @@ namespace Grav\Framework\Object\Base;
  */
 trait ObjectTrait
 {
-    static protected $prefix;
+    /** @var string */
     static protected $type;
 
     /**
@@ -23,18 +23,28 @@ trait ObjectTrait
      */
     private $_key;
 
+    /**
+     * @return string
+     */
+    protected function getTypePrefix()
+    {
+        return '';
+    }
+
     /**
      * @param bool $prefix
      * @return string
      */
     public function getType($prefix = true)
     {
+        $type = $prefix ? $this->getTypePrefix() : '';
+
         if (static::$type) {
-            return ($prefix ? static::$prefix : '') . static::$type;
+            return $type . static::$type;
         }
 
         $class = get_class($this);
-        return ($prefix ? static::$prefix : '') . strtolower(substr($class, strrpos($class, '\\') + 1));
+        return $type . strtolower(substr($class, strrpos($class, '\\') + 1));
     }
 
     /**
@@ -108,7 +118,7 @@ trait ObjectTrait
      */
     public function serialize()
     {
-        return serialize($this->jsonSerialize());
+        return serialize($this->doSerialize());
     }
 
     /**
@@ -124,6 +134,14 @@ trait ObjectTrait
         $this->doUnserialize($data);
     }
 
+    /**
+     * @return array
+     */
+    protected function doSerialize()
+    {
+        return $this->jsonSerialize();
+    }
+
     /**
      * @param array $serialized
      */
@@ -159,10 +177,13 @@ trait ObjectTrait
 
     /**
      * @param string $key
+     * @return $this
      */
     protected function setKey($key)
     {
         $this->_key = (string) $key;
+
+        return $this;
     }
 
     abstract protected function doHasProperty($property);

+ 198 - 0
system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php

@@ -0,0 +1,198 @@
+<?php
+/**
+ * @package    Grav\Framework\Object
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Object\Collection;
+
+use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
+use Doctrine\Common\Collections\Expr\Comparison;
+
+class ObjectExpressionVisitor extends ClosureExpressionVisitor
+{
+    /**
+     * Accesses the field of a given object.
+     *
+     * @param object $object
+     * @param string $field
+     *
+     * @return mixed
+     */
+    public static function getObjectFieldValue($object, $field)
+    {
+        $op = $value = null;
+
+        $pos = strpos($field, '(');
+        if (false !== $pos) {
+            list ($op, $field) = explode('(', $field, 2);
+            $field = rtrim($field, ')');
+        }
+
+        if (isset($object[$field])) {
+            $value = $object[$field];
+        } else {
+            $accessors = array('', 'get', 'is');
+
+            foreach ($accessors as $accessor) {
+                $accessor .= $field;
+
+                if (!method_exists($object, $accessor)) {
+                    continue;
+                }
+
+                $value = $object->{$accessor}();
+                break;
+            }
+        }
+
+        if ($op) {
+            $function = 'filter' . ucfirst(strtolower($op));
+            if (method_exists(static::class, $function)) {
+                $value = static::$function($value);
+            }
+        }
+
+        return $value;
+    }
+
+    public static function filterLower($str)
+    {
+        return mb_strtolower($str);
+    }
+
+    public static function filterUpper($str)
+    {
+        return mb_strtoupper($str);
+    }
+
+    public static function filterLength($str)
+    {
+        return mb_strlen($str);
+    }
+
+    public static function filterLtrim($str)
+    {
+        return ltrim($str);
+    }
+
+    public static function filterRtrim($str)
+    {
+        return rtrim($str);
+    }
+
+    public static function filterTrim($str)
+    {
+        return trim($str);
+    }
+
+    /**
+     * Helper for sorting arrays of objects based on multiple fields + orientations.
+     *
+     * @param string   $name
+     * @param int      $orientation
+     * @param \Closure $next
+     *
+     * @return \Closure
+     */
+    public static function sortByField($name, $orientation = 1, \Closure $next = null)
+    {
+        if (!$next) {
+            $next = function() {
+                return 0;
+            };
+        }
+
+        return function ($a, $b) use ($name, $next, $orientation) {
+            $aValue = static::getObjectFieldValue($a, $name);
+            $bValue = static::getObjectFieldValue($b, $name);
+
+            if ($aValue === $bValue) {
+                return $next($a, $b);
+            }
+
+            return (($aValue > $bValue) ? 1 : -1) * $orientation;
+        };
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function walkComparison(Comparison $comparison)
+    {
+        $field = $comparison->getField();
+        $value = $comparison->getValue()->getValue(); // shortcut for walkValue()
+
+        switch ($comparison->getOperator()) {
+            case Comparison::EQ:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) === $value;
+                };
+
+            case Comparison::NEQ:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) !== $value;
+                };
+
+            case Comparison::LT:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) < $value;
+                };
+
+            case Comparison::LTE:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) <= $value;
+                };
+
+            case Comparison::GT:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) > $value;
+                };
+
+            case Comparison::GTE:
+                return function ($object) use ($field, $value) {
+                    return static::getObjectFieldValue($object, $field) >= $value;
+                };
+
+            case Comparison::IN:
+                return function ($object) use ($field, $value) {
+                    return \in_array(static::getObjectFieldValue($object, $field), $value, true);
+                };
+
+            case Comparison::NIN:
+                return function ($object) use ($field, $value) {
+                    return !\in_array(static::getObjectFieldValue($object, $field), $value, true);
+                };
+
+            case Comparison::CONTAINS:
+                return function ($object) use ($field, $value) {
+                    return false !== strpos(static::getObjectFieldValue($object, $field), $value);
+                };
+
+            case Comparison::MEMBER_OF:
+                return function ($object) use ($field, $value) {
+                    $fieldValues = static::getObjectFieldValue($object, $field);
+                    if (!is_array($fieldValues)) {
+                        $fieldValues = iterator_to_array($fieldValues);
+                    }
+                    return \in_array($value, $fieldValues, true);
+                };
+
+            case Comparison::STARTS_WITH:
+                return function ($object) use ($field, $value) {
+                    return 0 === strpos(static::getObjectFieldValue($object, $field), $value);
+                };
+
+            case Comparison::ENDS_WITH:
+                return function ($object) use ($field, $value) {
+                    return $value === substr(static::getObjectFieldValue($object, $field), -strlen($value));
+                };
+
+
+            default:
+                throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator());
+        }
+    }
+}

+ 2 - 1
system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php

@@ -8,13 +8,14 @@
 
 namespace Grav\Framework\Object\Interfaces;
 
+use Doctrine\Common\Collections\Selectable;
 use Grav\Framework\Collection\CollectionInterface;
 
 /**
  * ObjectCollection Interface
  * @package Grav\Framework\Collection
  */
-interface ObjectCollectionInterface extends CollectionInterface, ObjectInterface
+interface ObjectCollectionInterface extends CollectionInterface, Selectable, ObjectInterface
 {
     /**
      * Create a copy from this collection by cloning all objects in the collection.

+ 35 - 0
system/src/Grav/Framework/Object/ObjectCollection.php

@@ -8,9 +8,11 @@
 
 namespace Grav\Framework\Object;
 
+use Doctrine\Common\Collections\Criteria;
 use Grav\Framework\Collection\ArrayCollection;
 use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;
 use Grav\Framework\Object\Base\ObjectCollectionTrait;
+use Grav\Framework\Object\Collection\ObjectExpressionVisitor;
 use Grav\Framework\Object\Interfaces\NestedObjectInterface;
 use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
 
@@ -36,6 +38,39 @@ class ObjectCollection extends ArrayCollection implements ObjectCollectionInterf
         $this->setKey($key);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function matching(Criteria $criteria)
+    {
+        $expr     = $criteria->getWhereExpression();
+        $filtered = $this->getElements();
+
+        if ($expr) {
+            $visitor  = new ObjectExpressionVisitor();
+            $filter   = $visitor->dispatch($expr);
+            $filtered = array_filter($filtered, $filter);
+        }
+
+        if ($orderings = $criteria->getOrderings()) {
+            $next = null;
+            foreach (array_reverse($orderings) as $field => $ordering) {
+                $next = ObjectExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
+            }
+
+            uasort($filtered, $next);
+        }
+
+        $offset = $criteria->getFirstResult();
+        $length = $criteria->getMaxResults();
+
+        if ($offset || $length) {
+            $filtered = array_slice($filtered, (int)$offset, $length);
+        }
+
+        return $this->createFrom($filtered);
+    }
+
     protected function getElements()
     {
         return $this->toArray();

+ 4 - 4
system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php

@@ -95,10 +95,10 @@ trait ObjectPropertyTrait
     }
 
     /**
-     * @param string $property      Object property to be fetched.
-     * @param mixed $default        Default value if property has not been set.
-     * @param bool $doCreate        Set true to create variable.
-     * @return mixed                Property value.
+     * @param string $property          Object property to be fetched.
+     * @param mixed $default            Default value if property has not been set.
+     * @param callable|bool $doCreate   Set true to create variable.
+     * @return mixed                    Property value.
      */
     protected function &doGetProperty($property, $default = null, $doCreate = false)
     {

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

@@ -178,7 +178,7 @@ class Route
      */
     public function withGravParam($param, $value)
     {
-        return $this->withParam('gravParams', $param, $value);
+        return $this->withParam('gravParams', $param, null !== $value ? (string)$value : null);
     }
 
     /**
@@ -222,17 +222,16 @@ class Route
     protected function withParam($type, $param, $value)
     {
         $oldValue = isset($this->{$type}[$param]) ? $this->{$type}[$param] : null;
-        $newValue = null !== $value ? (string)$value : null;
 
-        if ($oldValue === $newValue) {
+        if ($oldValue === $value) {
             return $this;
         }
 
         $new = clone $this;
-        if ($newValue === null) {
+        if ($value === null) {
             unset($new->{$type}[$param]);
         } else {
-            $new->{$type}[$param] = $newValue;
+            $new->{$type}[$param] = $value;
         }
 
         return $new;

+ 17 - 0
system/src/Grav/Framework/Route/RouteFactory.php

@@ -28,6 +28,23 @@ class RouteFactory
         return new Route($parts);
     }
 
+    public static function createFromString($path)
+    {
+        $path = ltrim($path, '/');
+        $parts = [
+            'path' => $path,
+            'query' => '',
+            'query_params' => [],
+            'grav' => [
+                'root' => self::$root,
+                'language' => self::$language,
+                'route' => $path,
+                'params' => ''
+            ],
+        ];
+        return new Route($parts);
+    }
+
     public static function getRoot()
     {
         return self::$root;

+ 340 - 0
system/src/Grav/Framework/Session/Session.php

@@ -0,0 +1,340 @@
+<?php
+/**
+ * @package    Grav\Framework\Session
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Session;
+
+/**
+ * Class Session
+ * @package Grav\Framework\Session
+ */
+class Session implements SessionInterface
+{
+    /**
+     * @var bool
+     */
+    protected $started = false;
+
+    /**
+     * @var Session
+     */
+    protected static $instance;
+
+    /**
+     * @inheritdoc
+     */
+    public static function getInstance()
+    {
+        if (null === self::$instance) {
+            throw new \RuntimeException("Session hasn't been initialized.", 500);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function __construct(array $options = [])
+    {
+        // Session is a singleton.
+        if (\PHP_SAPI === 'cli') {
+            self::$instance = $this;
+
+            return;
+        }
+
+        if (null !== self::$instance) {
+            throw new \RuntimeException('Session has already been initialized.', 500);
+        }
+
+        // Destroy any existing sessions started with session.auto_start
+        if ($this->isSessionStarted()) {
+            session_unset();
+            session_destroy();
+        }
+
+        // Set default options.
+        $options += array(
+            'cache_limiter' => 'nocache',
+            'use_trans_sid' => 0,
+            'use_cookies' => 1,
+            'lazy_write' => 1,
+            'use_strict_mode' => 1
+        );
+
+        $this->setOptions($options);
+
+        session_register_shutdown();
+
+        self::$instance = $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getId()
+    {
+        return session_id();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setId($id)
+    {
+        session_id($id);
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getName()
+    {
+        return session_name();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setName($name)
+    {
+        session_name($name);
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setOptions(array $options)
+    {
+        if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
+            return;
+        }
+
+        $allowedOptions = [
+            'save_path' => true,
+            'name' => true,
+            'save_handler' => true,
+            'gc_probability' => true,
+            'gc_divisor' => true,
+            'gc_maxlifetime' => true,
+            'serialize_handler' => true,
+            'cookie_lifetime' => true,
+            'cookie_path' => true,
+            'cookie_domain' => true,
+            'cookie_secure' => true,
+            'cookie_httponly' => true,
+            'use_strict_mode' => true,
+            'use_cookies' => true,
+            'use_only_cookies' => true,
+            'referer_check' => true,
+            'cache_limiter' => true,
+            'cache_expire' => true,
+            'use_trans_sid' => true,
+            'trans_sid_tags' => true,           // PHP 7.1
+            'trans_sid_hosts' => true,          // PHP 7.1
+            'sid_length' => true,               // PHP 7.1
+            'sid_bits_per_character' => true,   // PHP 7.1
+            'upload_progress.enabled' => true,
+            'upload_progress.cleanup' => true,
+            'upload_progress.prefix' => true,
+            'upload_progress.name' => true,
+            'upload_progress.freq' => true,
+            'upload_progress.min-freq' => true,
+            'lazy_write' => true,
+            'url_rewriter.tags' => true,        // Not used in PHP 7.1
+            'hash_function' => true,            // Not used in PHP 7.1
+            'hash_bits_per_character' => true,  // Not used in PHP 7.1
+            'entropy_file' => true,             // Not used in PHP 7.1
+            'entropy_length' => true,           // Not used in PHP 7.1
+        ];
+
+        foreach ($options as $key => $value) {
+            if (is_array($value)) {
+                // Allow nested options.
+                foreach ($value as $key2 => $value2) {
+                    $ckey = "{$key}.{$key2}";
+                    if (isset($value2, $allowedOptions[$ckey])) {
+                        $this->ini_set("session.{$ckey}", $value2);
+                    }
+                }
+            } elseif (isset($value, $allowedOptions[$key])) {
+                $this->ini_set("session.{$key}", $value);
+            }
+        }
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function start($readonly = false)
+    {
+        // 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()]);
+        }
+
+        $options = $readonly ? ['read_and_close' => '1'] : [];
+
+        $success = @session_start($options);
+        if (!$success) {
+            $last = error_get_last();
+            $error = $last ? $last['message'] : 'Unknown error';
+            throw new \RuntimeException('Failed to start session: ' . $error, 500);
+        }
+
+        $params = session_get_cookie_params();
+
+        setcookie(
+            session_name(),
+            session_id(),
+            time() + $params['lifetime'],
+            $params['path'],
+            $params['domain'],
+            $params['secure'],
+            $params['httponly']
+        );
+
+        $this->started = true;
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function invalidate()
+    {
+        $params = session_get_cookie_params();
+        setcookie(
+            session_name(),
+            '',
+            time() - 42000,
+            $params['path'],
+            $params['domain'],
+            $params['secure'],
+            $params['httponly']
+        );
+
+        session_unset();
+        session_destroy();
+
+        $this->started = false;
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function close()
+    {
+        if ($this->started) {
+            session_write_close();
+        }
+
+        $this->started = false;
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function clear()
+    {
+        session_unset();
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getAll()
+    {
+        return $_SESSION;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getIterator()
+    {
+        return new \ArrayIterator($_SESSION);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isStarted()
+    {
+        return $this->started;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function __isset($name)
+    {
+        return isset($_SESSION[$name]);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function __get($name)
+    {
+        return isset($_SESSION[$name]) ? $_SESSION[$name] : null;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function __set($name, $value)
+    {
+        $_SESSION[$name] = $value;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function __unset($name)
+    {
+        unset($_SESSION[$name]);
+    }
+
+    /**
+     * http://php.net/manual/en/function.session-status.php#113468
+     * Check if session is started nicely.
+     * @return bool
+     */
+    protected function isSessionStarted()
+    {
+        return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false;
+    }
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     */
+    protected function ini_set($key, $value)
+    {
+        if (!is_string($value)) {
+            if (is_bool($value)) {
+                $value = $value ? '1' : '0';
+            }
+            $value = (string)$value;
+        }
+
+        ini_set($key, $value);
+    }
+}

+ 147 - 0
system/src/Grav/Framework/Session/SessionInterface.php

@@ -0,0 +1,147 @@
+<?php
+/**
+ * @package    Grav\Framework\Session
+ *
+ * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Session;
+
+/**
+ * Class Session
+ * @package Grav\Framework\Session
+ */
+interface SessionInterface extends \IteratorAggregate
+{
+    /**
+     * Get current session instance.
+     *
+     * @return Session
+     * @throws \RuntimeException
+     */
+    public static function getInstance();
+
+    /**
+     * Get session ID
+     *
+     * @return string|null Session ID
+     */
+    public function getId();
+
+    /**
+     * Set session ID
+     *
+     * @param string $id Session ID
+     *
+     * @return $this
+     */
+    public function setId($id);
+
+    /**
+     * Get session name
+     *
+     * @return string|null
+     */
+    public function getName();
+
+    /**
+     * Set session name
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setName($name);
+
+    /**
+     * Sets session.* ini variables.
+     *
+     * @param array $options
+     *
+     * @see http://php.net/session.configuration
+     */
+    public function setOptions(array $options);
+
+    /**
+     * Starts the session storage
+     *
+     * @param bool $readonly
+     * @return $this
+     * @throws \RuntimeException
+     */
+    public function start($readonly = false);
+
+    /**
+     * Invalidates the current session.
+     *
+     * @return $this
+     */
+    public function invalidate();
+
+    /**
+     * Force the session to be saved and closed
+     *
+     * @return $this
+     */
+    public function close();
+
+    /**
+     * Free all session variables.
+     *
+     * @return $this
+     */
+    public function clear();
+
+    /**
+     * Returns all session variables.
+     *
+     * @return array
+     */
+    public function getAll();
+
+    /**
+     * Retrieve an external iterator
+     *
+     * @return \ArrayIterator Return an ArrayIterator of $_SESSION
+     */
+    public function getIterator();
+
+    /**
+     * Checks if the session was started.
+     *
+     * @return Boolean
+     */
+    public function isStarted();
+
+    /**
+     * Checks if session variable is defined.
+     *
+     * @param string $name
+     * @return bool
+     */
+    public function __isset($name);
+
+    /**
+     * Returns session variable.
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name);
+
+    /**
+     * Sets session variable.
+     *
+     * @param string $name
+     * @param mixed  $value
+     */
+    public function __set($name, $value);
+
+    /**
+     * Removes session variable.
+     *
+     * @param string $name
+     */
+    public function __unset($name);
+}

File diff suppressed because it is too large
+ 7 - 0
user/pages/01._recits/_12-juillet-2017/text.md


+ 13 - 0
user/pages/01._recits/_12-septembre-2017/text.md

@@ -0,0 +1,13 @@
+---
+title: '12 septembre 2017'
+image_align: left
+id: rct_12092017
+---
+
+Depuis quelques semaines, je suis en contact avec Manon, dont Wiebke de Point Carré m'avait donné le numéro de téléphone. Il y a quelques temps déjà, Manon avait mené un projet de cartographie du réseau artistique de Saint-Denis et il a donc semblé évident à Wiebke que nous aurions beaucoup à échanger. Elle ne s’était pas trompée.
+Après avoir eu un peu de mal à faire concorder nos disponibilités sur Saint-Denis, j'ai rejoint Manon un midi sur son lieu de travail, dans le quartier de La Défense à Paris.
+Installées sur un banc, je l’écoute me raconter comment elle est tombée sous le charme de Saint-Denis au cours du projet qu'elle y a mené :
+« L’idée de l’étude, c’était de comprendre les liens entre les lieux plutôt institutionnels et les lieux plutôt alternatifs. J’allai donc voir soit des lieux, soit des artistes, pour qu’ils me parlent de leur parcours, de leurs réseaux etc.. et c’était génial parce que je rencontrais de personnes en personnes. »
+C’est par le biais de toutes ces rencontres que Manon a pu avoir un apperçu de la richesse de projets, activités et émulations du territoire de Saint Denis.
+« Ce qui me plait aussi là bas c’est que par exemple si je vais au Pavillon je sais que je vais croiser des gens que je connais. J’y rejoins une amie, on est deux, on finit la soirée on est 10 à tables parce que les gens viennent discuter, tu t’es croisé une fois ou deux dans un projet du coin et on fini par boire des coups ensemble. «  Rapidement, elle a décidé de s’y installer et il lui a fallu moins de 8 mois pour se familiariser avec ce nouveau territoire.
+Lorsque Manon décrit sa "ville à echelle humaine" comme un espace pleins de ressources et pleins de vie, sa bienveillance à l’égard de sa nouvelle ville me donne envie de continuer à découvrir les lieux à travers son regard.  Le Pavillon, un café du centre ville, semble être l'un de ses lieux fétiche et je décide donc d’aller m’y installer l'après-midi même, le temps d'un café.

File diff suppressed because it is too large
+ 8 - 0
user/pages/01._recits/_13-septembre-2017/text.md


+ 24 - 0
user/pages/01._recits/_19-juin-2017/text.md

@@ -0,0 +1,24 @@
+---
+title: '19 juin 2017'
+image_align: left
+id: rct_190617
+---
+
+Il y a quelques jours, j'ai échangé avec [Pasacale](#int_pascale) dont j'avais eu vent du travail sur [l'affiche](#img_mj2) de la Maison Jaune.
+En début d'après midi, nous nous retrouvons à la Maison Jaune, où Pascale me fait découvrir le lieu. Elle m’explique : « C’est une maison de quartier qui a ouvert au mois de Novembre. l’idée c’est que cet espace soit mis à disposition des habitants, qu’on y organise des choses mais aussi qu’il y ai de la place pour leurs propres initiatives… »
+Pascale a découvert le lieu en octobre 2016, en passant devant. Elle n’habite pas loin et était justement à la recherche d’un lieu pour son projet de tricot-partage, un projet de tricot solidaire.
+Je lui demande de m’en dire plus sur son projet et fait vite le lien avec le patchwork de formes tricotés, monumental, qui git au sol de la Maison Jaune :
+«  Le projet de tricot solidaire c’était, au vu de la grosse vague de froid, de réaliser ,avec la participation du public, des carrés de laine de 30cm de côté assemblés en couvertures. Ces couvertures sont à destination de la Croix Rouge et distribuées lors des maraudes. L’envie était aussi que chacun, tout âge confondu, puisse participer à sa manière, en venant pour tricoter, pour apprendre ou pour transmettre. »
+Pour se fournir en laine, Pascale a lancé un appel au don dans Saint-Denis, qui a vite débordé du territoire par le biais d’internet. Elle a reçu des colis de Paris, de Bretagne, de Nice..
+« j’ai même eu un message d’une personne dans un petit village d’ardèche, qui m’a écrit en me disant qu’elle tricoterait des petits carrés de laine pour nous les envoyer. »
+Ici, internet est un vecteur indéniable de rencontre, mais le « fil participatif » comme l’appel Pascale, s’est construit de bien des manières. Le projet est nomade dans Saint-Denis, permettant au groupe de rencontrer d’avantage de personnes et d’ouvrir le réseau initial. Les moyens de communication du projet sont aussi divers. Les habitants de Saint-Denis ont vent du projet par des relais Facebook, des affichages en ville, des flyers.. jusqu’à l’impression même du flyer :
+« Il y a quelques temps, chez l’imprimeur, je faisais mes photocopies pour l’évenement à la Maison Jaune. Au moment de régler j’ai posé mes copies sur le comptoir. Une dame était derrière moi et a lu « tricot ». Elle m’a demandé ce qu’était l’événement et elle est venue. Depuis, elle vient régulièrement et nous sommes assez liées. »
+
+Dans le collectif, c’est sur l’application what’s app que la communication interne s'organise. Vers l’extérieur, c’est plutôt Facebook.
+Lorsque nous évoquons les moyens d’échange et de communication à disposition d’une association comme celle-ci, Pascale observe que l’outil numérique leur permet d’agréger des personnes qui ne sont pas forcément disponibles en temps et en heure,  ou n’ont pas la possibilité de se déplacer, mais qui souhaitent quand même être informés, participer et être tenus au courant des activités. 
+Elle note cependant que « la communication papier et la communication numérique sont vraiment complémentaires. Certains utilisent les deux régulièrement mais d’autres sont beaucoup moins familiers de l’outil informatique. Il y a des personnes qui n’ont pas d’e-mail et ne sont joignables que par téléphone, voire qui n’ont pas de téléphone portable. Je crois aussi que c’est bien de ne pas uniformiser l’information.»
+
+Pendant notre entretien, apparaît Sofiane, un habitant impliqué dans le collectif de la Maison Jaune, qui projette d'y organiser des ateliers de patisseries, étant lui-même en formation patissier. 
+On sent dans ce lieu l’émulation de quelque chose qui naît, qui ne sait pas toujours où il va mais qui est bien là.
+Ce premier échange a été très instructif et Pascale m’ayant évoqué plusieurs lieux de Saint-Denis, je profite de l'après-midi pour les repérer. En chemin, je croise des tricoteuses : un petit groupe tricote dans la rue, sur les arbres, les barrières. Ca fait tout de suite écho avec le projet tricot-partage de Pascale, et me pousse à m'arrêter discuter.
+C'est comme ça que je fais la connaissance d'Océane, de l'association Dechets d'arts, qui participe ici à une "opération street mamies", me dit-elle. Elle me laisse sa carte pour que nous nous recontactions plus tard.

File diff suppressed because it is too large
+ 7 - 0
user/pages/01._recits/_25-juillet-2017/text.md


File diff suppressed because it is too large
+ 6 - 0
user/pages/01._recits/_26-juillet-2017/text.md


File diff suppressed because it is too large
+ 10 - 0
user/pages/01._recits/_3-mai-2017/text.md


File diff suppressed because it is too large
+ 6 - 0
user/pages/01._recits/_3-octobre-2017/text.md


File diff suppressed because it is too large
+ 11 - 0
user/pages/01._recits/_4-octobre-2017/text.md


File diff suppressed because it is too large
+ 12 - 0
user/pages/01._recits/_5-octobre-2017/text.md


File diff suppressed because it is too large
+ 7 - 0
user/pages/01._recits/_5-septembre-2017/text.md


File diff suppressed because it is too large
+ 7 - 0
user/pages/01._recits/_6-juillet-2017/text.md


+ 12 - 0
user/pages/01._recits/_6-juin-2017/text.md

@@ -0,0 +1,12 @@
+---
+title: '6 juin 2017'
+image_align: left
+id: rct_060617
+---
+
+Je fais aujourd'hui la connaissance de [Loyce](), en stage à Synesthésie et étudiante à [Paris 8](). 
+Je teste avec elle le format d’interview que j'ai imaginé et la mise en contact avec une tierce personne. Elle m’envoie vers [Marie.P](), artiste et enseignante à Paris 8 : elle a été l'une de ses étudiantes.
+[Bachir,]() qui a justement travaillé avec elle lors d’un atelier à Synesthésie, nous met en lien directement par mail.
+Dans l'après-midi, je découvre l’emplacement de la [Maison Jaune](), alors fermée. Sur la devanture, une affiche présente un projet de [Tricot Partage](img_tp), porté par une certaine [Pascale](). Je retrouve les infos sur la[ page Facebook]() et contacte Pascale pour lui proposer une rencontre. 
+Peu familière de l’environnement, l’aspect labyrinthique de la [Dalle](), faite de coins et de recoins, ne m’invite pas à m’y ballader : la formation de l’espace accentue chez moi le sentiment d’être perdue.
+Avant de repartir, je déambulle dans le centre de Saint-Denis, marchant au hasard. J’essaye de me rendre plus familière du centre mais ai tendance à graviter peu loin de la [station de métro ]()que je connais. 

File diff suppressed because it is too large
+ 21 - 13
user/pages/02._interviews/_andrea/text.md


File diff suppressed because it is too large
+ 2 - 4
user/pages/02._interviews/_manon-dumond/text.md


File diff suppressed because it is too large
+ 12 - 0
user/pages/02._interviews/_marie-w/text.md


File diff suppressed because it is too large
+ 16 - 0
user/pages/02._interviews/_martine/text.md


File diff suppressed because it is too large
+ 6 - 0
user/pages/02._interviews/_nadia/text.md


File diff suppressed because it is too large
+ 14 - 0
user/pages/02._interviews/_oceane-et-laurent/text.md


File diff suppressed because it is too large
+ 8 - 0
user/pages/02._interviews/_olivier/text.md


File diff suppressed because it is too large
+ 7 - 0
user/pages/02._interviews/_pascale/text.md


File diff suppressed because it is too large
+ 11 - 0
user/pages/02._interviews/_solen/text.md


File diff suppressed because it is too large
+ 14 - 0
user/pages/02._interviews/_victoria/text.md


BIN
user/pages/03._images/_rue-marie-w/DSC04024.JPG


+ 8 - 0
user/pages/03._images/_rue-marie-w/image.md

@@ -0,0 +1,8 @@
+---
+title: 'Rue Marie.W'
+media_order: DSC04024.JPG
+image_align: left
+id: img_ruemariew
+---
+
+![](DSC04024.JPG)

BIN
user/pages/03._images/_taxiphone/DSC04028.JPG


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