Browse Source

import from la bonne adresse and first refactoring for ouidade.com

Bachir Soussi Chiadmi 6 years ago
commit
344dae1543
100 changed files with 12233 additions and 0 deletions
  1. 5 0
      .gitignore
  2. 63 0
      .htaccess
  3. 892 0
      CHANGELOG.md
  4. 21 0
      LICENSE
  5. 5 0
      README.md
  6. 0 0
      assets/.gitkeep
  7. 0 0
      backup/.gitkeep
  8. BIN
      bin/composer.phar
  9. 56 0
      bin/gpm
  10. 46 0
      bin/grav
  11. 36 0
      composer.json
  12. 1012 0
      composer.lock
  13. 8 0
      fixperms.sh
  14. 63 0
      htaccess.txt
  15. 42 0
      index.php
  16. 87 0
      nginx.conf
  17. 2 0
      robots.txt
  18. BIN
      screenshot.png
  19. 54 0
      system/assets/debugger.css
  20. BIN
      system/assets/grav.png
  21. 1 0
      system/assets/jquery/jquery-2.1.4.min.js
  22. BIN
      system/assets/responsive-overlays/1x.png
  23. BIN
      system/assets/responsive-overlays/2x.png
  24. BIN
      system/assets/responsive-overlays/3x.png
  25. BIN
      system/assets/responsive-overlays/4x.png
  26. BIN
      system/assets/responsive-overlays/unknown.png
  27. 110 0
      system/assets/whoops.css
  28. 5 0
      system/blueprints/config/media.yaml
  29. 116 0
      system/blueprints/config/site.yaml
  30. 7 0
      system/blueprints/config/streams.yaml
  31. 793 0
      system/blueprints/config/system.yaml
  32. 276 0
      system/blueprints/pages/default.yaml
  33. 47 0
      system/blueprints/pages/modular.yaml
  34. 56 0
      system/blueprints/pages/modular_new.yaml
  35. 97 0
      system/blueprints/pages/modular_raw.yaml
  36. 17 0
      system/blueprints/pages/move.yaml
  37. 66 0
      system/blueprints/pages/new.yaml
  38. 96 0
      system/blueprints/pages/raw.yaml
  39. 56 0
      system/blueprints/user/account.yaml
  40. 16 0
      system/blueprints/user/account_new.yaml
  41. 190 0
      system/config/media.yaml
  42. 34 0
      system/config/site.yaml
  43. 21 0
      system/config/streams.yaml
  44. 112 0
      system/config/system.yaml
  45. 42 0
      system/defines.php
  46. 37 0
      system/languages/cs.yaml
  47. 43 0
      system/languages/de.yaml
  48. 94 0
      system/languages/en.yaml
  49. 60 0
      system/languages/fr.yaml
  50. 21 0
      system/languages/it.yaml
  51. 43 0
      system/languages/nl.yaml
  52. 43 0
      system/languages/ru.yaml
  53. 1143 0
      system/src/Grav/Common/Assets.php
  54. 130 0
      system/src/Grav/Common/Backup/ZipBackup.php
  55. 58 0
      system/src/Grav/Common/Browser.php
  56. 314 0
      system/src/Grav/Common/Cache.php
  57. 55 0
      system/src/Grav/Common/Composer.php
  58. 207 0
      system/src/Grav/Common/Config/Blueprints.php
  59. 479 0
      system/src/Grav/Common/Config/Config.php
  60. 186 0
      system/src/Grav/Common/Config/ConfigFinder.php
  61. 27 0
      system/src/Grav/Common/Config/Languages.php
  62. 456 0
      system/src/Grav/Common/Data/Blueprint.php
  63. 145 0
      system/src/Grav/Common/Data/Blueprints.php
  64. 240 0
      system/src/Grav/Common/Data/Data.php
  65. 68 0
      system/src/Grav/Common/Data/DataInterface.php
  66. 68 0
      system/src/Grav/Common/Data/DataMutatorTrait.php
  67. 672 0
      system/src/Grav/Common/Data/Validation.php
  68. 121 0
      system/src/Grav/Common/Debugger.php
  69. 52 0
      system/src/Grav/Common/Errors/Errors.php
  70. 52 0
      system/src/Grav/Common/Errors/Resources/error.css
  71. 30 0
      system/src/Grav/Common/Errors/Resources/layout.html.php
  72. 96 0
      system/src/Grav/Common/Errors/SimplePageHandler.php
  73. 73 0
      system/src/Grav/Common/File/CompiledFile.php
  74. 9 0
      system/src/Grav/Common/File/CompiledMarkdownFile.php
  75. 9 0
      system/src/Grav/Common/File/CompiledYamlFile.php
  76. 353 0
      system/src/Grav/Common/Filesystem/Folder.php
  77. 31 0
      system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
  78. 32 0
      system/src/Grav/Common/GPM/AbstractCollection.php
  79. 34 0
      system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php
  80. 21 0
      system/src/Grav/Common/GPM/Common/CachedCollection.php
  81. 42 0
      system/src/Grav/Common/GPM/Common/Package.php
  82. 398 0
      system/src/Grav/Common/GPM/GPM.php
  83. 343 0
      system/src/Grav/Common/GPM/Installer.php
  84. 17 0
      system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php
  85. 32 0
      system/src/Grav/Common/GPM/Local/Package.php
  86. 17 0
      system/src/Grav/Common/GPM/Local/Packages.php
  87. 22 0
      system/src/Grav/Common/GPM/Local/Plugins.php
  88. 22 0
      system/src/Grav/Common/GPM/Local/Themes.php
  89. 58 0
      system/src/Grav/Common/GPM/PackageInterface.php
  90. 55 0
      system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php
  91. 95 0
      system/src/Grav/Common/GPM/Remote/Grav.php
  92. 12 0
      system/src/Grav/Common/GPM/Remote/Package.php
  93. 17 0
      system/src/Grav/Common/GPM/Remote/Packages.php
  94. 24 0
      system/src/Grav/Common/GPM/Remote/Plugins.php
  95. 24 0
      system/src/Grav/Common/GPM/Remote/Themes.php
  96. 221 0
      system/src/Grav/Common/GPM/Response.php
  97. 93 0
      system/src/Grav/Common/GPM/Upgrader.php
  98. 150 0
      system/src/Grav/Common/Getters.php
  99. 510 0
      system/src/Grav/Common/Grav.php
  100. 29 0
      system/src/Grav/Common/GravTrait.php

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+cache
+logs
+images
+user/pages
+node_modules

+ 63 - 0
.htaccess

@@ -0,0 +1,63 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+## Begin RewriteBase
+# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
+# You should change the '/' to your appropriate subfolder. For example if you have
+# your Grav install at the root of your site '/' should work, else it might be something
+# along the lines of: RewriteBase /<your_sub_folder>
+##
+
+# RewriteBase /
+
+## End - RewriteBase
+
+## Begin - Exploits
+# If you experience problems on your site block out the operations listed below
+# This attempts to block the most common type of exploit `attempts` to Grav
+#
+# Block out any script trying to base64_encode data within the URL.
+RewriteCond %{QUERY_STRING} base64_encode[^(]*\([^)]*\) [OR]
+# Block out any script that includes a <script> tag in URL.
+RewriteCond %{QUERY_STRING} (<|%3C)([^s]*s)+cript.*(>|%3E) [NC,OR]
+# Block out any script trying to set a PHP GLOBALS variable via URL.
+RewriteCond %{QUERY_STRING} GLOBALS(=|\[|\%[0-9A-Z]{0,2}) [OR]
+# Block out any script trying to modify a _REQUEST variable via URL.
+RewriteCond %{QUERY_STRING} _REQUEST(=|\[|\%[0-9A-Z]{0,2})
+# Return 403 Forbidden header and show the content of the root homepage
+RewriteRule .* index.php [F]
+#
+## End - Exploits
+
+## Begin - Index
+# If the requested path and file is not /index.php and the request
+# has not already been internally rewritten to the index.php script
+RewriteCond %{REQUEST_URI} !^/index\.php
+# and the requested path and file doesn't directly match a physical file
+RewriteCond %{REQUEST_FILENAME} !-f
+# and the requested path and file doesn't directly match a physical folder
+RewriteCond %{REQUEST_FILENAME} !-d
+# internally rewrite the request to the index.php script
+RewriteRule .* index.php [L]
+## End - Index
+
+## Begin - Security
+# Block all direct access for these folders
+RewriteRule ^(.git|cache|bin|logs|backup)/(.*) error [L]
+# Block access to specific file types for these folders
+RewriteRule ^(system|user|vendor)/(.*)\.(txt|md|html|yaml|php|twig|sh|bat)$ error [L]
+# Block all direct access to .md files:
+RewriteRule \.md$ error [L]
+# Block all direct access to files and folders beginning with a dot
+RewriteRule (^\.|/\.) - [F]
+# Block access to specific files in the root folder
+RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config)$ error [F]
+## End - Security
+
+</IfModule>
+
+# Begin - Prevent Browsing and Set Default Resources
+Options -Indexes
+DirectoryIndex index.php index.html index.htm
+# End - Prevent Browsing and Set Default Resources

+ 892 - 0
CHANGELOG.md

@@ -0,0 +1,892 @@
+# v1.0.0-rc.4
+## 10/29/2015
+
+1. [](#bugfix)
+    * Fixed a fatal error if you have a collection with missing or invalid `@page: /route`
+
+# v1.0.0-rc.3
+## 10/29/2015
+
+1. [](#new)
+    * New Page collection options! `@self.parent, @self.siblings, @self.descendants` + more
+    * Whitelist of file types for fallback route functionality (images by default)
+1. [](#improved)
+    * Assets switched from defines to streams
+1. [](#bugfix)
+    * README.md typos fixed
+    * Fixed issue with routes that have lang string in them (`/en/english`)
+    * Trim strings before validation so whitespace is not satisfy 'required'
+
+# v1.0.0-rc.2
+## 10/27/2015
+
+1. [](#new)
+    * Added support for CSS Asset groups
+    * Added a `wrapped_site` system option for themes/plugins to use
+    * Pass `Page` object as event to `onTwigPageVariables()` event hook
+    * New `Data.items()` method to get all items
+1. [](#improved)
+    * Missing pipelined remote asset will now fail quietly
+    * More reliably handle inline JS and CSS to remove only surrounding HTML tags
+    * `Medium.meta` returns new Data object so null checks are possible
+    * Improved Medium metadata merging to allow for automatic title/alt/class attributes
+    * Moved Grav object to global variable rather than template variable (useful for macros)
+    * German language improvements
+    * Updated bundled composer
+1. [](#bugfix)
+    * Accept variety of `true` values in `User.authorize()` method 
+    * Fix for `Validation` throwing an error if no label set
+
+# v1.0.0-rc.1
+## 10/23/2015
+
+1. [](#new)
+    * Use native PECL YAML parser if installed for 4X speed boost in parsing YAML files
+    * Support for inherited theme class
+    * Added new default language prepend system configuration option
+    * New `|evaluate` Twig filter to evaluate a string as twig
+    * New system option to ignore all **hidden** files and folders
+    * New system option for default redirect code
+    * Added ability to append specific `[30x]` codes to redirect URLs
+    * Added `url_taxonomy_filters` for page collections
+    * Added `@root` page and `recurse` flag for page collections
+    * Support for **multiple** page collection types as an array
+    * Added Dutch language file
+    * Added Russian language file
+    * Added `remove` method to User object
+1. [](#improved)
+    * Moved hardcoded mimetypes to `media.yaml` to be treated as Page media files
+    * Set `errors: display: false` by default in `system.yaml`
+    * Strip out extra slashes in the URI
+    * Validate hostname to ensure it is valid
+    * Ignore more SCM folders in Backups
+    * Removed `home_redirect` settings from `system.yaml`
+    * Added Page `media` as root twig object for consistency
+    * Updated to latest vendor libraries
+    * Optimizations to Asset pipeline logic for minor speed increase
+    * Block direct access to a variety of files in `.htaccess` for increased security
+    * Debugbar vendor library update
+    * Always fallback to english if other translations are not available
+1. [](#bugfix)
+    * Fix for redirecting external URL with multi-language
+    * Fix for Asset pipeline not respecting asset groups
+    * Fix language files with child/parent theme relationships
+    * Fixed a regression issue resulting in incorrect default language
+    * Ensure error handler is initialized before URI is processed
+    * Use default language in Twig if active language is not set
+    * Fixed issue with `safeEmailFilter()` Twig filter not separating with `;` properly
+    * Fixed empty YAML file causing error with native PECL YAML parser
+    * Fixed `SVG` mimetype
+    * Fixed incorrect `Cache-control: max-age` value format
+
+# v0.9.45
+## 10/08/2015
+
+1. [](#bugfix)
+    * Fixed a regression issue resulting in incorrect default language
+
+# v0.9.44
+## 10/07/2015
+
+1. [](#new)
+    * Added Redis back as a supported cache mechanism
+    * Allow Twig `nicetime` translations
+    * Added `-y` option for 'Yes to all' in `bin/gpm update`
+    * Added CSS `media` attribute to the Assets manager
+    * New German language support
+    * New Czech language support
+    * New French language support
+    * Added `modulus` twig filter
+1. [](#improved)
+    * URL decode in medium actions to allow complex syntax
+    * Take into account `HTTP_HOST` before `SERVER_NAME` (helpful with Nginx)
+    * More friendly cache naming to ease manual management of cache systems
+    * Added default Apache resource for `DirectoryIndex`
+1. [](#bugfix)
+    * Fix GPM failure when offline
+    * Fix `open_basedir` error in `bin/gpm install`
+    * Fix an HHVM error in Truncator
+    * Fix for XSS vulnerability with params
+    * Fix chaining for responsive size derivatives
+    * Fix for saving pages when removing the page title and all other header elements
+    * Fix when saving array fields
+    * Fix for ports being included in `HTTP_HOST`
+    * Fix for Truncator to handle PHP tags gracefully
+    * Fix for locate style lang codes in `getNativeName()`
+    * Urldecode image basenames in markdown
+
+# v0.9.43
+## 09/16/2015
+
+1. [](#new)
+    * Added new `AudioMedium` for HTML5 audio
+    * Added ability for Assets to be added and displayed in separate *groups*
+    * New support for responsive image derivative sizes
+1. [](#improved)
+    * GPM theme install now uses a `copy` method so new files are not lost (e.g. `/css/custom.css`)
+    * Code analysis improvements and cleanup
+    * Removed Twig panel from debugger (no longer supported in Twig 1.20)
+    * Updated composer packages
+    * Prepend active language to `convertUrl()` when used in markdown links
+    * Added some pre/post flight options for installer via blueprints
+    * Hyphenize the site name in the backup filename
+1. [](#bugfix)
+    * Fix broken routable logic
+    * Check for `phpinfo()` method in case it is restricted by hosting provider
+    * Fixes for windows when running GPM
+    * Fix for ampersand (`&`) causing error in `truncateHtml()` via `Page.summary()`
+
+# v0.9.42
+## 09/11/2015
+
+1. [](#bugfix)
+    * Fixed `User.authorise()` to be backwards compabile
+
+# v0.9.41
+## 09/11/2015
+
+1. [](#new)
+    * New and improved multibyte-safe TruncateHTML function and filter
+    * Added support for custom page date format
+    * Added a `string` Twig filter to render as json_encoded string
+    * Added `authorize` Twig filter
+    * Added support for theme inheritance in the admin
+    * Support for multiple content collections on a page
+    * Added configurable files/folders ignores for pages
+    * Added the ability to set the default PHP locale and override via multi-lang configuration
+    * Added ability to save as YAML via admin
+    * Added check for `mbstring` support
+    * Added new `redirect` header for pages
+1. [](#improved)
+    * Changed dependencies from `develop` to `master`
+    * Updated logging to log everything from `debug` level on (was `warning`)
+    * Added missing `accounts/` folder
+    * Default to performing a 301 redirect for URIs with trailing slashes
+    * Improved Twig error messages
+    * Allow validating of forms from anywhere such as plugins
+    * Added logic so modular pages are by default non-routable
+    * Hide password input in `bin/grav newuser` command
+1. [](#bugfix)
+    * Fixed `Pages.all()` not returning modular pages
+    * Fix for modular template types not getting found
+    * Fix for `markdown_extra:` overriding `markdown:extra:` setting
+    * Fix for multi-site routing
+    * Fix for multi-lang page name error
+    * Fixed a redirect loop in `URI` class
+    * Fixed a potential error when `unsupported_inline_types` is empty
+    * Correctly generate 2x retina image
+    * Typo fixes in page publish/unpublish blueprint
+
+# v0.9.40
+## 08/31/2015
+
+1. [](#new)
+    * Added some new Twig filters: `defined`, `rtrim`, `ltrim`
+    * Admin support for customizable page file name + template override
+1. [](#improved)
+    * Better message for incompatible/unsupported Twig template
+    * Improved User blueprints with better help
+    * Switched to composer **install** rather than **update** by default
+    * Admin autofocus on page title
+    * `.htaccess` hardening (`.htaccess` & `htaccess.txt`)
+    * Cache safety checks for missing folders
+1. [](#bugfix)
+    * Fixed issue with unescaped `o` character in date formats
+
+# v0.9.39
+## 08/25/2015
+
+1. [](#bugfix)
+    * `Page.active()` not triggering on **homepage**
+    * Fix for invalid session name in Opera browser
+
+# v0.9.38
+## 08/24/2015
+
+1. [](#new)
+    * Added `language` to **user** blueprint
+    * Added translations to blueprints
+    * New extending logic for blueprints
+    * Blueprints are now loaded with Streams to allow for better overrides
+    * Added new Symfony `dump()` method
+1. [](#improved)
+    * Catch YAML header parse exception so site doesn't die
+    * Better `Page.parent()` logic
+    * Improved GPM display layout
+    * Tweaked default page layout
+    * Unset route and slug for improved reliability of route changes
+    * Added requirements to README.md
+    * Updated various libraries
+    * Allow use of custom page date field for dateRange collections
+1. [](#bugfix)
+    * Slug fixes with GPM
+    * Unset plaintext password on save
+    * Fix for trailing `/` not matching active children
+
+# v0.9.37
+## 08/12/2015
+
+3. [](#bugfix)
+    * Fixed issue when saving `header.process` in page forms via the **admin plugin**
+    * Fixed error due to use of `set_time_limit` that might be disabled on some hosts
+
+# v0.9.36
+## 08/11/2015
+
+1. [](#new)
+    * Added a new `newuser` CLI command to create user accounts
+    * Added `default` blueprint for all templates
+    * Support `user` and `system` language translation merging
+1. [](#improved)
+    * Added isSymlink method in GPM to determine if Grav is symbolically linked or not
+    * Refactored page recursing
+    * Updated blueprints to use new toggles
+    * Updated blueprints to use current date for date format fields
+    * Updated composer.phar
+    * Use sessions for admin even when disabled for site
+    * Use `GRAV_ROOT` in session identifier
+
+# v0.9.35
+## 08/06/2015
+
+1. [](#new)
+    * Added `body_classes` field
+    * Added `visiblity` toggle and help tooltips on new page form
+    * Added new `Page.unsetRoute()` method to allow admin to regenerate the route
+2. [](#improved)
+    * User save no longer stores username each time
+    * Page list form field now shows all pages except root
+    * Removed required option from page title
+    * Added configuration settings for running Nginx in sub directory
+3. [](#bugfix)
+    * Fixed deep translation merging
+    * Fixed broken **metadata** merging with site defaults
+    * Fixed broken **summary** field
+    * Fixed broken robots field
+    * Fixed GPM issue when using cURL, throwing an `Undefined offset: 1` exception
+    * Removed duplicate hidden page `type` field
+
+# v0.9.34
+## 08/04/2015
+
+1. [](#new)
+    * Added new `cache_all` system setting + media `cache()` method
+    * Added base languages configuration
+    * Added property language to page to help plugins identify page language
+    * New `Utils::arrayFilterRecursive()` method
+2. [](#improved)
+    * Improved Session handling to support site and admin independently
+    * Allow Twig variables to be modified in other events
+    * Blueprint updates in preparation for Admin plugin
+    * Changed `Inflector` from static to object and added multi-language support
+    * Support for admin override of a page's blueprints
+3. [](#bugfix)
+    * Removed unused `use` in `VideoMedium` that was causing error
+    * Array fix in `User.authorise()` method
+    * Fix for typo in `translations_fallback`
+    * Fixed moving page to the root
+
+# v0.9.33
+## 07/21/2015
+
+1. [](#new)
+    * Added new `onImageMediumSaved()` event (useful for post-image processing)
+    * Added `Vary: Accept-Encoding` option
+2. [](#improved)
+    * Multilang-safe delimeter position
+    * Refactored Twig classes and added optional umask setting
+    * Removed `pageinit()` timing
+    * `Page->routable()` now takes `published()` state into account
+    * Improved how page extension is set
+    * Support `Language->translate()` method taking string and array
+3. [](#bugfix)
+    * Fixed `backup` command to include empty folders
+
+# v0.9.32
+## 07/14/2015
+
+1. [](#new)
+    * Detect users preferred language via `http_accept_language` setting
+    * Added new `translateArray()` language method
+2. [](#improved)
+    * Support `en` translations by default for plugins & themes
+    * Improved default generator tag
+    * Minor language tweaks and fixes
+3. [](#bugfix)
+    * Fix for session active language and homepage redirects
+    * Ignore root-level page rather than throwing error
+
+# v0.9.31
+## 07/09/2015
+
+1. [](#new)
+    * Added xml, json, css and js to valid media file types
+2. [](#improved)
+    * Better handling of unsupported media type downloads
+    * Improved `bin/grav backup` command to mimic admin plugin location/name
+3. [](#bugfix)
+    * Critical fix for broken language translations
+    * Fix for Twig markdown filter error
+    * Safety check for download extension
+
+# v0.9.30
+## 07/08/2015
+
+1. [](#new)
+    * BIG NEWS! Extensive Multi-Language support is all new in 0.9.30!
+    * Translation support via Twig filter/function and PHP method
+    * Page specific default route
+    * Page specific route aliases
+    * Canonical URL route support
+    * Added built-in session support
+    * New `Page.rawRoute()` to get a consistent folder-based route to a page
+    * Added option to always redirect to default page on alias URL
+    * Added language safe redirect function for use in core and plugins
+2. [](#improved)
+    * Improved `Page.active()` and `Page.activeChild()` methods to support route aliases
+    * Various spelling corrections in `.php` comments, `.md` and `.yaml` files
+    * `Utils::startsWith()` and `Utils::endsWith()` now support needle arrays
+    * Added a new timer around `pageInitialized` event
+    * Updated jQuery library to v2.1.4
+3. [](#bugfix)
+    * In-page CSS and JS files are now handled properly
+    * Fix for `enable_media_timestamp` not working properly
+
+# v0.9.29
+## 06/22/2015
+
+1. [](#new)
+    * New and improved Regex-powered redirect and route alias logic
+    * Added new `onBuildPagesInitialized` event for memory critical or time-consuming plugins
+    * Added a `setSummary()` method for pages
+2. [](#improved)
+    * Improved `MergeConfig()` logic for more control
+    * Travis skeleton build trigger implemented
+    * Set composer.json versions to stable versions where possible
+    * Disabled `last_modified` and `etag` page headers by default (causing too much page caching)
+3. [](#bugfix)
+    * Preload classes during `bin/gpm selfupgrade` to avoid issues with updated classes
+    * Fix for directory relative _down_ links
+
+# v0.9.28
+## 06/16/2015
+
+1. [](#new)
+    * Added method to set raw markdown on a page
+    * Added ability to enabled system and page level `etag` and `last_modified` headers
+2. [](#improved)
+    * Improved image path processing
+    * Improved query string handling
+    * Optimization to image handling supporting URL encoded filenames
+    * Use global `composer` when available rather than Grv provided one
+    * Use `PHP_BINARY` contant rather than `php` executable
+    * Updated Doctrine Cache library
+    * Updated Symfony libraries
+    * Moved `convertUrl()` method to Uri object
+3. [](#bugfix)
+    * Fix incorrect slug causing problems with CLI `uninstall`
+    * Fix Twig runtime error with assets pipeline in sufolder installations
+    * Fix for `+` in image filenames
+    * Fix for dot files causing issues with page processing
+    * Fix for Uri path detection on Windows platform
+    * Fix for alternative media resolutions
+    * Fix for modularTypes key properties
+
+# v0.9.27
+## 05/09/2015
+
+1. [](#new)
+    * Added new composer CLI command
+    * Added page-level summary header overrides
+    * Added `size` back for Media objects
+    * Refactored Backup command in preparation for admin plugin
+    * Added a new `parseLinks` method to Plugins class
+    * Added `starts_with` and `ends_with` Twig filters
+2. [](#improved)
+    * Optimized install of vendor libraries for speed improvement
+    * Improved configuration handling in preparation for admin plugin
+    * Cache optimization: Don't cache Twig templates when you pass dynamic params
+    * Moved `Utils::rcopy` to `Folder::rcopy`
+    * Improved `Folder::doDelete`
+    * Added check for required Curl in GPM
+    * Updated included composer.phar to latest version
+    * Various blueprint fixes for admin plugin
+    * Various PSR and code cleanup tasks
+3. [](#bugfix)
+    * Fix issue with Gzip not working with `onShutDown()` event
+    * Fix for URLs with trailing slashes
+    * Handle condition where certain errors resulted in blank page
+    * Fix for issue with theme name equal to base_url and asset pipeline
+    * Fix to properly normalize font rewrite path
+    * Fix for absolute URLs below the current page
+    * Fix for `..` page references
+
+# v0.9.26
+## 04/24/2015
+
+3. [](#bugfix)
+    * Fixed issue with homepage routes failing with 'dirname' error
+
+# v0.9.25
+## 04/24/2015
+
+1. [](#new)
+    * Added support for E-Tag, Last-Modified, Cache-Control and Page-based expires headers
+2. [](#improved)
+    * Refactored media image handling to make it more flexible and support absolute paths
+    * Refactored page modification check process to make it faster
+    * User account improvements in preparation for admin plugin
+    * Protect against timing attacks
+    * Reset default system expires time to 0 seconds (can override if you need to)
+3. [](#bugfix)
+    * Fix issues with spaces in webroot when using `bin/grav install`
+    * Fix for spaces in relative directory
+    * Bug fix in collection filtering
+
+# v0.9.24
+## 04/15/2015
+
+1. [](#new)
+    * Added support for chunked downloads of Assets
+    * Added new `onBeforeDownload()` event
+    * Added new `download()` and `getMimeType()` methods to Utils class
+    * Added configuration option for supported page types
+    * Added assets and media timestamp options (off by default)
+    * Added page expires configuration option
+2. [](#bugfix)
+    * Fixed issue with Nginx/Gzip and `ob_flush()` throwing error
+    * Fixed assets actions on 'direct media' URLs
+    * Fix for 'direct assets` with any parameters
+
+# v0.9.23
+## 04/09/2015
+
+1. [](#bugfix)
+    * Fix for broken GPM `selfupgrade` (Grav 0.9.21 and 0.9.22 will need to manually upgrade to this version)
+
+# v0.9.22
+## 04/08/2015
+
+1. [](#bugfix)
+    * Fix to normalize GRAV_ROOT path for Windows
+    * Fix to normalize Media image paths for Windows
+    * Fix for GPM `selfupgrade` when you are on latest version
+
+# v0.9.21
+## 04/07/2015
+
+1. [](#new)
+    * Major Media functionality enhancements: SVG, Animated GIF, Video support!
+    * Added ability to configure default image quality in system configuration
+    * Added `sizes` attributes for custom retina image breakpoints
+2. [](#improved)
+    * Don't scale @1x retina images
+    * Add filter to Iterator class
+    * Updated various composer packages
+    * Various PSR fixes
+
+# v0.9.20
+## 03/24/2015
+
+1. [](#new)
+    * Added `addAsyncJs()` and `addDeferJs()` to Assets manager
+    * Added support for extranal URL redirects
+2. [](#improved)
+    * Fix unpredictable asset ordering when set from plugin/system
+    * Updated `nginx.conf` to ensure system assets are accessible
+    * Ensure images are served as static files in Nginx
+    * Updated vendor libraries to latest versions
+    * Updated included composer.phar to latest version
+3. [](#bugfix)
+    * Fixed issue with markdown links to `#` breaking HTML
+
+# v0.9.19
+## 02/28/2015
+
+1. [](#new)
+    * Added named assets capability and bundled jQuery into Grav core
+    * Added `first()` and `last()` to `Iterator` class
+2. [](#improved)
+    * Improved page modification routine to skip _dot files_
+    * Only use files to calculate page modification dates
+    * Broke out Folder iterators into their own classes
+    * Various Sensiolabs Insight fixes
+3. [](#bugfix)
+    * Fixed `Iterator.nth()` method
+
+# v0.9.18
+## 02/19/2015
+
+1. [](#new)
+    * Added ability for GPM `install` to automatically install `_demo` content if found (w/backup)
+    * Added ability for themes and plugins to have dependencies required to install via GPM
+    * Added ability to override the system timezone rather than relying on server setting only
+    * Added new Twig filter `random_string` for generating random id values
+    * Added new Twig filter `markdown` for on-the-fly markdown processing
+    * Added new Twig filter `absoluteUrl` to convert relative to absolute URLs
+    * Added new `processTemplate()` method to Twig object for on-the-fly processing of twig template
+    * Added `rcopy()` and `contains()` helper methods in Utils
+2. [](#improved)
+    * Provided new `param_sep` variable to better support Apache on Windows
+    * Moved parsedown configuration into the trait
+    * Added optional **deep-copy** option to `mergeConfig()` for plugins
+    * Updated bundled `composer.phar` package
+    * Various Sensiolabs Insight fixes - Silver level now!
+    * Various PSR Fixes
+3. [](#bugfix)
+    * Fix for windows platforms not displaying installed themes/plugins via GPM
+    * Fix page IDs not picking up folder-only pages
+
+# v0.9.17
+## 02/05/2015
+
+1. [](#new)
+    * Added **full HHVM support!** Get your speed on with Facebook's crazy fast PHP JIT compiler
+2. [](#improved)
+    * More flexible page summary control
+    * Support **CamelCase** plugin and theme class names. Replaces dashes and underscores
+    * Moved summary delimiter into `site.yaml` so it can be configurable
+    * Various PSR fixes
+3. [](#bugfix)
+     * Fix for `mergeConfig()` not falling back to defaults
+     * Fix for `addInlineCss()` and `addInlineJs()` Assets not working between Twig tags
+     * Fix for Markdown adding HTML tags into inline CSS and JS
+
+# v0.9.16
+## 01/30/2015
+
+1. [](#new)
+    * Added **Retina** and **Responsive** image support via Grav media and `srcset` image attribute
+    * Added image debug option that overlays responsive resolution
+    * Added a new image cache stream
+2. [](#improved)
+    * Improved the markdown Lightbox functionality to better mimic Twig version
+    * Fullsize Lightbox can now have filters applied
+    * Added a new `mergeConfig()` method to Plugin class to merge system + page header configuration
+    * Added a new `disable()` method to Plugin class to programmatically disable a plugin
+    * Updated Parsedown and Parsedown Extra to address bugs
+    * Various PSR fixes
+3. [](#bugfix)
+     * Fix bug with image dispatch in traditionally _non-routable_ pages
+     * Fix for markdown link not working on non-current pages
+     * Fix for markdown images not being found on homepage
+
+# v0.9.15
+## 01/23/2015
+
+3. [](#bugfix)
+     * Typo in video mime types
+     * Fix for old `markdown_extra` system setting not getting picked up
+     * Fix in regex for Markdown links with numeric values in path
+     * Fix for broken image routing mechanism that got broken at some point
+     * Fix for markdown images/links in pages with page slug override
+
+# v0.9.14
+## 01/23/2015
+
+1. [](#new)
+    * Added **GZip** support
+    * Added multiple configurations via `setup.php`
+    * Added base structure for unit tests
+    * New `onPageContentRaw()` plugin event that processes before any page processing
+    * Added ability to dynamically set Metadata on page
+    * Added ability to dynamically configure Markdown processing via Parsedown options
+2. [](#improved)
+    * Refactored `page.content()` method to be more flexible and reliable
+    * Various updates and fixes for streams resulting in better multi-site support
+    * Updated Twig, Parsedown, ParsedownExtra, DoctrineCache libraries
+    * Refactored Parsedown trait
+    * Force modular pages to be non-visible in menus
+    * Moved RewriteBase before Exploits in `.htaccess`
+    * Added standard video formats to Media support
+    * Added priority for inline assets
+    * Check for uniqueness when adding multiple inline assets
+    * Improved support for Twig-based URLs inside Markdown links and images
+    * Improved Twig `url()` function
+3. [](#bugfix)
+    * Fix for HTML entities quotes in Metadata values
+    * Fix for `published` setting to have precedent of `publish_date` and `unpublish_date`
+    * Fix for `onShutdown()` events not closing connections properly in **php-fpm** environments
+
+# v0.9.13
+## 01/09/2015
+
+1. [](#new)
+    * Added new published `true|false` state in page headers
+    * Added `publish_date` in page headers to automatically publish page
+    * Added `unpublish_date` in page headers to automatically unpublish page
+    * Added `dateRange()` capability for collections
+    * Added ability to dynamically control Cache lifetime programmatically
+    * Added ability to sort by anything in the page header. E.g. `sort: header.taxonomy.year`
+    * Added various helper methods to collections: `copy, nonVisible, modular, nonModular, published, nonPublished, nonRoutable`
+2. [](#improved)
+    * Modified all Collection methods so they can be chained together: `$collection->published()->visible()`
+    * Set default Cache lifetime to default of 1 week (604800 seconds) - was infinite
+    * House-cleaning of some unused methods in Pages object
+3. [](#bugfix)
+    * Fix `uninstall` GPM command that was broken in last release
+    * Fix for intermittent `undefined index` error when working with Collections
+    * Fix for date of some pages being set to incorrect future timestamps
+
+# v0.9.12
+## 01/06/2015
+
+1. [](#new)
+    * Added an all-access robots.txt file for search engines
+    * Added new GPM `uninstall` command
+    * Added support for **in-page** Twig processing in **modular** pages
+    * Added configurable support for `undefined` Twig functions and filters
+2. [](#improved)
+    * Fall back to default `.html` template if error occurs on non-html pages
+    * Added ability to have PSR-1 friendly plugin names (CamelCase, no-dashes)
+    * Fix to `composer.json` to deter API rate-limit errors
+    * Added **non-exception-throwing** handler for undefined methods on `Medium` objects
+3. [](#bugfix)
+    * Fix description for `self-upgrade` method of GPM command
+    * Fix for incorrect version number when performing GPM `update`
+    * Fix for argument description of GPM `install` command
+    * Fix for recalcitrant CodeKit mac application
+
+# v0.9.11
+## 12/21/2014
+
+1. [](#new)
+    * Added support for simple redirects as well as routes
+2. [](#improved)
+    * Handle Twig errors more cleanly
+3. [](#bugfix)
+    * Fix for error caused by invalid or missing user agent string
+    * Fix for directory relative links and URL fragments (#pagelink)
+    * Fix for relative links with no subfolder in `base_url`
+
+# v0.9.10
+## 12/12/2014
+
+1. [](#new)
+    * Added Facebook-style `nicetime` date Twig filter
+2. [](#improved)
+    * Moved `clear-cache` functionality into Cache object required for Admin plugin
+3. [](#bugfix)
+    * Fix for undefined index with previous/next buttons
+
+# v0.9.9
+## 12/05/2014
+
+1. [](#new)
+    * Added new `@page` collection type
+    * Added `ksort` and `contains` Twig filters
+    * Added `gist` Twig function
+2. [](#improved)
+    * Refactored Page previous/next/adjacent functionality
+    * Updated to Symfony 2.6 for yaml/console/event-dispatcher libraries
+    * More PSR code fixes
+3. [](#bugfix)
+    * Fix for over-escaped apostrophes in YAML
+
+# v0.9.8
+## 12/01/2014
+
+1. [](#new)
+    * Added configuration option to set default lifetime on cache saves
+    * Added ability to set HTTP status code from page header
+    * Implemented simple wild-card custom routing
+2. [](#improved)
+    * Fixed elusive double load to fully cache issue (crossing fingers...)
+    * Ensure Twig tags are treated as block items in markdown
+    * Removed some older deprecated methods
+    * Ensure onPageContentProcessed() event only fires when not cached
+    * More PSR code fixes
+3. [](#bugfix)
+    * Fix issue with miscalculation of blog separator location `===`
+
+# v0.9.7
+## 11/24/2014
+
+1. [](#improved)
+    * Nginx configuration updated
+    * Added gitter.im badge to README
+    * Removed `set_time_limit()` and put checks around `ignore_user_abort`
+    * More PSR code fixes
+2. [](#bugfix)
+    * Fix issue with non-valid asset path showing up when they shouldn't
+    * Fix for JS asset pipeline and scripts that don't end in `;`
+    * Fix for schema-based markdown URLs broken routes (eg `mailto:`)
+
+# v0.9.6
+## 11/17/2014
+
+1. [](#improved)
+    * Moved base_url variables into Grav container
+    * Forced media sorting to use natural sort order by default
+    * Various PSR code tidying
+    * Added filename, extension, thumb to all medium objects
+2. [](#bugfix)
+    * Fix for infinite loop in page.content()
+    * Fix hostname for configuration overrides
+    * Fix for cached configuration
+    * Fix for relative URLs in markdown on installs with no base_url
+    * Fix for page media images with uppercase extension
+
+# v0.9.5
+## 11/09/2014
+
+1. [](#new)
+    * Added quality setting to medium for compression configuration of images
+    * Added new onPageContentProcessed() event that is post-content processing but pre-caching
+2. [](#improved)
+    * Added support for AND and OR taxonomy filtering.  AND by default (was OR)
+    * Added specific clearing options for CLI clear-cache command
+    * Moved environment method to URI so it can be accessible in plugins and themes
+    * Set Grav's output variable to public so it can be manipulated in onOutputGenerated event
+    * Updated vendor libraries to latest versions
+    * Better handing of 'home' in active menu state detection
+    * Various PSR code tidying
+    * Improved some error messages and notices
+3. [](#bugfix)
+    * Force route rebuild when configuration changes
+    * Fix for 'installed undefined' error in CLI versions command
+    * Do not remove the JSON/Text error handlers
+    * Fix for supporting inline JS and CSS when Asset pipeline enabled
+    * Fix for Data URLs in CSS being badly formed
+    * Fix Markdown links with fragment and query elements
+
+# v0.9.4
+## 10/29/2014
+
+1. [](#new)
+    * New improved Debugbar with messages, timing, config, twig information
+    * New exception handling system utilizing Whoops
+    * New logging system utilizing Monolog
+    * Support for auto-detecting environment configuration
+    * New version command for CLI
+    * Integrate Twig dump() calls into Debugbar
+2. [](#improved)
+    * Selfupgrade now clears cache on successful upgrade
+    * Selfupgrade now supports files without extensions
+    * Improved error messages when plugin is missing
+    * Improved security in .htaccess
+    * Support CSS/JS/Image assets in vendor/system folders via .htaccess
+    * Add support for system timers
+    * Improved and optimized configuration loading
+    * Automatically disable Debugbar on non-HTML pages
+    * Disable Debugbar by default
+3. [](#bugfix)
+    * More YAML blueprint fixes
+    * Fix potential double // in assets
+    * Load debugger as early as possible
+
+# v0.9.3
+## 10/09/2014
+
+1. [](#new)
+    * GPM (Grav Package Manager) Added
+    * Support for multiple Grav configurations
+    * Dynamic media support via URL
+    * Added inlineCss and inlineJs support for Assets
+2. [](#improved)
+    * YAML caching for increased performance
+    * Use stream wrapper in pages, plugins and themes
+    * Switched to RocketTheme toolbox for some core functionality
+    * Renamed `setup` CLI command to `sandbox`
+    * Broke cache types out into multiple directories in the cache folder
+    * Removed vendor libs from github repository
+    * Various PSR cleanup of code
+    * Various Blueprint updates to support upcoming admin plugin
+    * Added ability to filter page children for normal/modular/all
+    * Added `sort_by_key` twig filter
+    * Added `visible()` and `routable()` filters to page collections
+    * Use session class in shutdown process
+    * Improvements to modular page loading
+    * Various code cleanup and optimizations
+3. [](#bugfix)
+    * Fixed file checking not updating the last modified time. For real this time!
+    * Switched debugger to PRODUCTION mode by default
+    * Various fixes in URI class for increased reliability
+
+# v0.9.2
+## 09/15/2014
+
+1. [](#new)
+    * New flexible site and page metadata support including ObjectGraph and Facebook
+    * New method to get user IP address in URI object
+    * Added new onShutdown() event that fires after connection is closed for Async features
+2. [](#improved)
+    * Skip assets pipeline minify on Windows platforms by default due to PHP issue 47689
+    * Fixed multiple level menus not highlighting correctly
+    * Updated some blueprints in preparation for admin plugin
+    * Fail gracefully when theme does not exist
+    * Add stream support into ResourceLocator::addPath()
+    * Separate themes from plugins, add themes:// stream and onTask events
+    * Added barDump() to Debugger
+    * Removed stray test page
+    * Override modified only if a non-markdown file was modified
+    * Added assets attributes support
+    * Auto-run composer install when running the Grav CLI
+    * Vendor folder removed from repository
+    * Minor configuration performance optimizations
+    * Minor debugger performance optimizations
+3. [](#bugfix)
+    * Fix url() twig function when Grav isn't installed at root
+    * Workaround for PHP bug 52065
+    * Fixed getList() method on Pages object that was not working
+    * Fix for open_basedir error
+    * index.php now warns if not running on PHP 5.4
+    * Removed memcached option (redundant)
+    * Removed memcache from auto setup, added memcache server configuration option
+    * Fix broken password validation
+    * Back to proper PSR-4 Autoloader
+
+# v0.9.1
+## 09/02/2014
+
+1. [](#new)
+    * Added new `theme://` PHP stream for current theme
+2. [](#improved)
+    * Default to new `file` modification checking rather than `folder`
+    * Added support for various markdown link formats to convert to Grav-friendly URLs
+    * Moved configure() from Theme to Themes class
+    * Fix autoloading without composer update -o
+    * Added support for Twig url method
+    * Minor code cleanup
+3. [](#bugfix)
+    * Fixed issue with page changes not being picked up
+    * Fixed Minify to provide `@supports` tag compatibility
+    * Fixed ResourceLocator not working with multiple paths
+    * Fixed issue with Markdown process not stripping LFs
+    * Restrict file type extensions for added security
+    * Fixed template inheritance
+    * Moved Browser class to proper location
+
+# v0.9.0
+## 08/25/2014
+
+1. [](#new)
+    * Addition of Dependency Injection Container
+    * Refactored plugins to use Symfony Event Dispatcher
+    * New Asset Manager to provide unified management of JavaScript and CSS
+    * Asset Pipelining to provide unification, minify, and optimization of JavaScript and CSS
+    * Grav Media support directly in Markdown syntax
+    * Additional Grav Generator meta tag in default themes
+    * Added support for PHP Stream Wrapper for resource location
+    * Markdown Extra support
+    * Browser object for fast browser detection
+2. [](#improved)
+    * PSR-4 Autoloader mechanism
+    * Tracy Debugger new `detect` option to detect running environment
+    * Added new `random` collection sort option
+    * Make media images progressive by default
+    * Additional URI filtering for improved security
+    * Safety checks to ensure PHP 5.4.0+
+    * Move to Slidebars side navigation in default Antimatter theme
+    * Updates to `.htaccess` including section on `RewriteBase` which is needed for some hosting providers
+3. [](#bugfix)
+    * Fixed issue when installing in an apache userdir (~username) folder
+    * Various mobile CSS issues in default themes
+    * Various minor bug fixes
+
+
+# v0.8.0
+## 08/13/2014
+
+1. [](#new)
+    * Initial Release

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Grav
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# [ouidade.com](http://ouidade.com)/
+
+website build with [grav](http://getgrav.org)
+
+![screenshot](screenshot.png)

+ 0 - 0
assets/.gitkeep


+ 0 - 0
backup/.gitkeep


BIN
bin/composer.phar


+ 56 - 0
bin/gpm

@@ -0,0 +1,56 @@
+#!/usr/bin/env php
+<?php
+define('GRAV_CLI', true);
+
+if (version_compare($ver = PHP_VERSION, $req = '5.4.0', '<')) {
+    exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
+}
+
+if (!file_exists(__DIR__ . '/../vendor')){
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+}
+
+use Grav\Common\Composer;
+
+if (!file_exists(__DIR__ . '/../vendor')){
+    // Before we can even start, we need to run composer first
+    $composer = Composer::getComposerExecutor();
+    echo "Preparing to install vendor dependencies...\n\n";
+    echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
+    echo "\n\n";
+}
+
+use Symfony\Component\Console\Application;
+use Grav\Common\Grav;
+
+$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
+
+if (!ini_get('date.timezone')) {
+    date_default_timezone_set('UTC');
+}
+
+if (!file_exists(ROOT_DIR . 'index.php')) {
+    exit('FATAL: Must be run from ROOT directory of Grav!');
+}
+
+if (!function_exists('curl_version')) {
+    exit('FATAL: GPM requires PHP Curl module to be installed');
+}
+
+$grav = Grav::instance(array('loader' => $autoload));
+$grav['config']->init();
+$grav['streams'];
+$grav['plugins']->init();
+$grav['themes']->init();
+
+$app = new Application('Grav Package Manager', GRAV_VERSION);
+$app->addCommands(array(
+    new \Grav\Console\Gpm\IndexCommand(),
+    new \Grav\Console\Gpm\VersionCommand(),
+    new \Grav\Console\Gpm\InfoCommand(),
+    new \Grav\Console\Gpm\InstallCommand(),
+    new \Grav\Console\Gpm\UninstallCommand(),
+    new \Grav\Console\Gpm\UpdateCommand(),
+    new \Grav\Console\Gpm\SelfupgradeCommand(),
+));
+$app->run();

+ 46 - 0
bin/grav

@@ -0,0 +1,46 @@
+#!/usr/bin/env php
+<?php
+define('GRAV_CLI', true);
+
+if (version_compare($ver = PHP_VERSION, $req = '5.4.0', '<')) {
+    exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
+}
+
+if (!file_exists(__DIR__ . '/../vendor')){
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+}
+
+use Grav\Common\Composer;
+
+if (!file_exists(__DIR__ . '/../vendor')){
+    // Before we can even start, we need to run composer first
+    $composer = Composer::getComposerExecutor();
+    echo "Preparing to install vendor dependencies...\n\n";
+    echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
+    echo "\n\n";
+}
+
+use Symfony\Component\Console\Application;
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+if (!ini_get('date.timezone')) {
+    date_default_timezone_set('UTC');
+}
+
+if (!file_exists(ROOT_DIR . 'index.php')) {
+    exit('FATAL: Must be run from ROOT directory of Grav!');
+}
+
+$app = new Application('Grav CLI Application', '0.1.0');
+$app->addCommands(array(
+    new Grav\Console\Cli\InstallCommand(),
+    new Grav\Console\Cli\ComposerCommand(),
+    new Grav\Console\Cli\SandboxCommand(),
+    new Grav\Console\Cli\CleanCommand(),
+    new Grav\Console\Cli\ClearCacheCommand(),
+    new Grav\Console\Cli\BackupCommand(),
+    new Grav\Console\Cli\NewProjectCommand(),
+    new Grav\Console\Cli\NewUserCommand(),
+));
+$app->run();

+ 36 - 0
composer.json

@@ -0,0 +1,36 @@
+{
+    "name": "getgrav/grav",
+    "type": "library",
+    "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS",
+    "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"],
+    "homepage": "http://getgrav.org",
+    "license": "MIT",
+    "require": {
+        "php": ">=5.4.0",
+        "twig/twig": "~1.16",
+        "erusev/parsedown-extra": "~0.7",
+        "symfony/yaml": "~2.7",
+        "symfony/console": "~2.7",
+        "symfony/event-dispatcher": "~2.7",
+        "symfony/var-dumper": "~2.7",
+        "doctrine/cache": "~1.4",
+        "filp/whoops": "1.2.*@dev",
+        "monolog/monolog": "~1.0",
+        "gregwar/image": "~2.0",
+        "ircmaxell/password-compat": "1.0.*",
+        "mrclay/minify": "~2.2",
+        "donatj/phpuseragentparser": "~0.3",
+        "pimple/pimple": "~3.0",
+        "rockettheme/toolbox": "1.1.*",
+        "maximebf/debugbar": "~1.10"
+    },
+    "autoload": {
+        "psr-4": {
+            "Grav\\": "system/src/Grav"
+        },
+        "files": ["system/defines.php"]
+    },
+    "archive": {
+        "exclude": ["VERSION"]
+    }
+}

+ 1012 - 0
composer.lock

@@ -0,0 +1,1012 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "This file is @generated automatically"
+    ],
+    "hash": "e1db721096772d41f16003b39b47c85a",
+    "packages": [
+        {
+            "name": "doctrine/cache",
+            "version": "v1.4.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/cache.git",
+                "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca",
+                "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "conflict": {
+                "doctrine/common": ">2.2,<2.4"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=3.7",
+                "predis/predis": "~1.0",
+                "satooshi/php-coveralls": "~0.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Cache\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Caching library offering an object-oriented API for many cache backends",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "cache",
+                "caching"
+            ],
+            "time": "2015-08-31 12:36:41"
+        },
+        {
+            "name": "donatj/phpuseragentparser",
+            "version": "v0.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/donatj/PhpUserAgent.git",
+                "reference": "1acea75664179c8f0dcd57ced7e75a01af86bfa8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/1acea75664179c8f0dcd57ced7e75a01af86bfa8",
+                "reference": "1acea75664179c8f0dcd57ced7e75a01af86bfa8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "camspiers/json-pretty": "0.1.*",
+                "donatj/drop": "*",
+                "phpunit/phpunit": "4.*"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Source/UserAgentParser.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jesse G. Donat",
+                    "email": "donatj@gmail.com",
+                    "homepage": "http://donatstudios.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Simple, streamlined PHP user-agent parser",
+            "homepage": "http://donatstudios.com/PHP-Parser-HTTP_USER_AGENT",
+            "keywords": [
+                "browser",
+                "browser detection",
+                "parser",
+                "user agent",
+                "useragent"
+            ],
+            "time": "2015-09-22 21:04:13"
+        },
+        {
+            "name": "erusev/parsedown",
+            "version": "1.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/erusev/parsedown.git",
+                "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/erusev/parsedown/zipball/3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7",
+                "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Parsedown": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Emanuil Rusev",
+                    "email": "hello@erusev.com",
+                    "homepage": "http://erusev.com"
+                }
+            ],
+            "description": "Parser for Markdown.",
+            "homepage": "http://parsedown.org",
+            "keywords": [
+                "markdown",
+                "parser"
+            ],
+            "time": "2015-10-04 16:44:32"
+        },
+        {
+            "name": "erusev/parsedown-extra",
+            "version": "0.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/erusev/parsedown-extra.git",
+                "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/11a44e076d02ffcc4021713398a60cd73f78b6f5",
+                "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5",
+                "shasum": ""
+            },
+            "require": {
+                "erusev/parsedown": "~1.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "ParsedownExtra": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Emanuil Rusev",
+                    "email": "hello@erusev.com",
+                    "homepage": "http://erusev.com"
+                }
+            ],
+            "description": "An extension of Parsedown that adds support for Markdown Extra.",
+            "homepage": "https://github.com/erusev/parsedown-extra",
+            "keywords": [
+                "markdown",
+                "markdown extra",
+                "parsedown",
+                "parser"
+            ],
+            "time": "2015-01-25 14:52:34"
+        },
+        {
+            "name": "filp/whoops",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/filp/whoops.git",
+                "reference": "9a393ceb80f7497b6513feb574638e87048fed55"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/9a393ceb80f7497b6513feb574638e87048fed55",
+                "reference": "9a393ceb80f7497b6513feb574638e87048fed55",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "0.9.*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Whoops": "src/"
+                },
+                "classmap": [
+                    "src/deprecated"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Filipe Dobreira",
+                    "homepage": "https://github.com/filp",
+                    "role": "Developer"
+                }
+            ],
+            "description": "php error handling for cool kids",
+            "homepage": "https://github.com/filp/whoops",
+            "keywords": [
+                "error",
+                "exception",
+                "handling",
+                "library",
+                "silex-provider",
+                "whoops",
+                "zf2"
+            ],
+            "time": "2015-09-27 09:47:06"
+        },
+        {
+            "name": "gregwar/cache",
+            "version": "v1.0.10",
+            "target-dir": "Gregwar/Cache",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Gregwar/Cache.git",
+                "reference": "0a1a02e4943e95f491b3d2395609247385975622"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Gregwar/Cache/zipball/0a1a02e4943e95f491b3d2395609247385975622",
+                "reference": "0a1a02e4943e95f491b3d2395609247385975622",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Gregwar\\Cache": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gregwar",
+                    "email": "g.passault@gmail.com"
+                }
+            ],
+            "description": "A lightweight file-system cache system",
+            "keywords": [
+                "cache",
+                "caching",
+                "file-system",
+                "system"
+            ],
+            "time": "2014-09-24 11:23:30"
+        },
+        {
+            "name": "gregwar/image",
+            "version": "v2.0.20",
+            "target-dir": "Gregwar/Image",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Gregwar/Image.git",
+                "reference": "2c6bf2fb3b0eb844f0568d6ee55eeb86fc799414"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Gregwar/Image/zipball/2c6bf2fb3b0eb844f0568d6ee55eeb86fc799414",
+                "reference": "2c6bf2fb3b0eb844f0568d6ee55eeb86fc799414",
+                "shasum": ""
+            },
+            "require": {
+                "ext-gd": "*",
+                "gregwar/cache": "1.*",
+                "php": ">=5.3.0"
+            },
+            "suggest": {
+                "behat/transliterator": "Transliterator provides ability to set non-latin1 pretty names"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Gregwar\\Image": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Grégoire Passault",
+                    "email": "g.passault@gmail.com",
+                    "homepage": "http://www.gregwar.com/"
+                }
+            ],
+            "description": "Image handling",
+            "homepage": "https://github.com/Gregwar/Image",
+            "keywords": [
+                "gd",
+                "image"
+            ],
+            "time": "2015-05-30 19:24:37"
+        },
+        {
+            "name": "ircmaxell/password-compat",
+            "version": "v1.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ircmaxell/password_compat.git",
+                "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c",
+                "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c",
+                "shasum": ""
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "lib/password.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Anthony Ferrara",
+                    "email": "ircmaxell@php.net",
+                    "homepage": "http://blog.ircmaxell.com"
+                }
+            ],
+            "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash",
+            "homepage": "https://github.com/ircmaxell/password_compat",
+            "keywords": [
+                "hashing",
+                "password"
+            ],
+            "time": "2014-11-20 16:49:30"
+        },
+        {
+            "name": "maximebf/debugbar",
+            "version": "v1.10.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/maximebf/php-debugbar.git",
+                "reference": "30e53e8a28284b69dd223c9f5ee8957befd72636"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30e53e8a28284b69dd223c9f5ee8957befd72636",
+                "reference": "30e53e8a28284b69dd223c9f5ee8957befd72636",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "psr/log": "~1.0",
+                "symfony/var-dumper": "~2.6"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "suggest": {
+                "kriswallsmith/assetic": "The best way to manage assets",
+                "monolog/monolog": "Log using Monolog",
+                "predis/predis": "Redis storage"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.10-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "DebugBar": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Maxime Bouroumeau-Fuseau",
+                    "email": "maxime.bouroumeau@gmail.com",
+                    "homepage": "http://maximebf.com"
+                }
+            ],
+            "description": "Debug bar in the browser for php application",
+            "homepage": "https://github.com/maximebf/php-debugbar",
+            "keywords": [
+                "debug"
+            ],
+            "time": "2015-10-19 20:35:12"
+        },
+        {
+            "name": "monolog/monolog",
+            "version": "1.17.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bee7f0dc9c3e0b69a6039697533dca1e845c8c24",
+                "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "psr/log": "~1.0"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^2.4.9",
+                "doctrine/couchdb": "~1.0@dev",
+                "graylog2/gelf-php": "~1.0",
+                "jakub-onderka/php-parallel-lint": "0.9",
+                "php-console/php-console": "^3.1.3",
+                "phpunit/phpunit": "~4.5",
+                "phpunit/phpunit-mock-objects": "2.3.0",
+                "raven/raven": "^0.13",
+                "ruflin/elastica": ">=0.90 <3.0",
+                "swiftmailer/swiftmailer": "~5.3",
+                "videlalvaro/php-amqplib": "~2.4"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "php-console/php-console": "Allow sending log messages to Google Chrome",
+                "raven/raven": "Allow sending log messages to a Sentry server",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+                "videlalvaro/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.16.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "http://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "time": "2015-10-14 12:51:02"
+        },
+        {
+            "name": "mrclay/minify",
+            "version": "2.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mrclay/minify.git",
+                "reference": "3c11ba8232a2155a1a29552aafc528be5fb0a662"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mrclay/minify/zipball/3c11ba8232a2155a1a29552aafc528be5fb0a662",
+                "reference": "3c11ba8232a2155a1a29552aafc528be5fb0a662",
+                "shasum": ""
+            },
+            "require": {
+                "ext-pcre": "*",
+                "php": ">=5.2.1"
+            },
+            "require-dev": {
+                "tubalmartin/cssmin": "~2.4.8"
+            },
+            "suggest": {
+                "tubalmartin/cssmin": "Support minify with CSSMin (YUI PHP port)"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "min/lib/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Stephen Clay",
+                    "email": "steve@mrclay.org",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Minify is a PHP5 app that helps you follow several rules for client-side performance. It combines multiple CSS or Javascript files, removes unnecessary whitespace and comments, and serves them with gzip encoding and optimal client-side cache headers",
+            "homepage": "http://code.google.com/p/minify/",
+            "time": "2014-10-30 22:58:02"
+        },
+        {
+            "name": "pimple/pimple",
+            "version": "v3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/silexphp/Pimple.git",
+                "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a",
+                "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Pimple": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                }
+            ],
+            "description": "Pimple, a simple Dependency Injection Container",
+            "homepage": "http://pimple.sensiolabs.org",
+            "keywords": [
+                "container",
+                "dependency injection"
+            ],
+            "time": "2015-09-11 15:10:35"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b",
+                "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Psr\\Log\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2012-12-21 11:40:51"
+        },
+        {
+            "name": "rockettheme/toolbox",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/rockettheme/toolbox.git",
+                "reference": "ff677d8f66d1addd3590d0cb85bcbaff4174d9c9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/ff677d8f66d1addd3590d0cb85bcbaff4174d9c9",
+                "reference": "ff677d8f66d1addd3590d0cb85bcbaff4174d9c9",
+                "shasum": ""
+            },
+            "require": {
+                "ircmaxell/password-compat": "1.0.*",
+                "php": ">=5.4.0",
+                "pimple/pimple": "~3.0",
+                "symfony/event-dispatcher": "~2.5",
+                "symfony/yaml": "~2.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.0.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "RocketTheme\\Toolbox\\ArrayTraits\\": "ArrayTraits/src",
+                    "RocketTheme\\Toolbox\\Blueprints\\": "Blueprints/src",
+                    "RocketTheme\\Toolbox\\DI\\": "DI/src",
+                    "RocketTheme\\Toolbox\\Event\\": "Event/src",
+                    "RocketTheme\\Toolbox\\File\\": "File/src",
+                    "RocketTheme\\Toolbox\\ResourceLocator\\": "ResourceLocator/src",
+                    "RocketTheme\\Toolbox\\Session\\": "Session/src",
+                    "RocketTheme\\Toolbox\\StreamWrapper\\": "StreamWrapper/src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "RocketTheme Toolbox Library",
+            "homepage": "http://www.rockettheme.com",
+            "keywords": [
+                "php",
+                "rockettheme"
+            ],
+            "time": "2015-10-15 23:27:40"
+        },
+        {
+            "name": "symfony/console",
+            "version": "v2.7.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/console.git",
+                "reference": "06cb17c013a82f94a3d840682b49425cd00a2161"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/console/zipball/06cb17c013a82f94a3d840682b49425cd00a2161",
+                "reference": "06cb17c013a82f94a3d840682b49425cd00a2161",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "psr/log": "~1.0",
+                "symfony/event-dispatcher": "~2.1",
+                "symfony/phpunit-bridge": "~2.7",
+                "symfony/process": "~2.1"
+            },
+            "suggest": {
+                "psr/log": "For using the console logger",
+                "symfony/event-dispatcher": "",
+                "symfony/process": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Console\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Console Component",
+            "homepage": "https://symfony.com",
+            "time": "2015-09-25 08:32:23"
+        },
+        {
+            "name": "symfony/event-dispatcher",
+            "version": "v2.7.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher.git",
+                "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9",
+                "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "psr/log": "~1.0",
+                "symfony/config": "~2.0,>=2.0.5",
+                "symfony/dependency-injection": "~2.6",
+                "symfony/expression-language": "~2.6",
+                "symfony/phpunit-bridge": "~2.7",
+                "symfony/stopwatch": "~2.3"
+            },
+            "suggest": {
+                "symfony/dependency-injection": "",
+                "symfony/http-kernel": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony EventDispatcher Component",
+            "homepage": "https://symfony.com",
+            "time": "2015-09-22 13:49:29"
+        },
+        {
+            "name": "symfony/var-dumper",
+            "version": "v2.7.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/var-dumper.git",
+                "reference": "ba8c9a0edf18f70a7efcb8d3eb35323a10263338"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ba8c9a0edf18f70a7efcb8d3eb35323a10263338",
+                "reference": "ba8c9a0edf18f70a7efcb8d3eb35323a10263338",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
+            },
+            "suggest": {
+                "ext-symfony_debug": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "Resources/functions/dump.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\VarDumper\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony mechanism for exploring and dumping PHP variables",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "debug",
+                "dump"
+            ],
+            "time": "2015-09-22 14:41:01"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v2.7.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770",
+                "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Yaml Component",
+            "homepage": "https://symfony.com",
+            "time": "2015-09-14 14:14:09"
+        },
+        {
+            "name": "twig/twig",
+            "version": "v1.22.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/twigphp/Twig.git",
+                "reference": "ebfc36b7e77b0c1175afe30459cf943010245540"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/ebfc36b7e77b0c1175afe30459cf943010245540",
+                "reference": "ebfc36b7e77b0c1175afe30459cf943010245540",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2.7"
+            },
+            "require-dev": {
+                "symfony/debug": "~2.7",
+                "symfony/phpunit-bridge": "~2.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.22-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Twig_": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com",
+                    "homepage": "http://fabien.potencier.org",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Armin Ronacher",
+                    "email": "armin.ronacher@active-4.com",
+                    "role": "Project Founder"
+                },
+                {
+                    "name": "Twig Team",
+                    "homepage": "http://twig.sensiolabs.org/contributors",
+                    "role": "Contributors"
+                }
+            ],
+            "description": "Twig, the flexible, fast, and secure template language for PHP",
+            "homepage": "http://twig.sensiolabs.org",
+            "keywords": [
+                "templating"
+            ],
+            "time": "2015-10-13 07:07:02"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {
+        "filp/whoops": 20
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=5.4.0"
+    },
+    "platform-dev": []
+}

+ 8 - 0
fixperms.sh

@@ -0,0 +1,8 @@
+#!/bin/sh
+sudo chown bach:http .
+sudo chown -R bach:http *
+sudo sh -c "find . -type f | xargs chmod 664"
+sudo sh -c "find ./bin -type f | xargs chmod 775"
+sudo sh -c "find . -type d | xargs chmod 775"
+sudo sh -c "find . -type d | xargs chmod +s"
+sudo sh -c"umask 0002"

+ 63 - 0
htaccess.txt

@@ -0,0 +1,63 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+## Begin RewriteBase
+# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
+# You should change the '/' to your appropriate subfolder. For example if you have
+# your Grav install at the root of your site '/' should work, else it might be something
+# along the lines of: RewriteBase /<your_sub_folder>
+##
+
+# RewriteBase /
+
+## End - RewriteBase
+
+## Begin - Exploits
+# If you experience problems on your site block out the operations listed below
+# This attempts to block the most common type of exploit `attempts` to Grav
+#
+# Block out any script trying to base64_encode data within the URL.
+RewriteCond %{QUERY_STRING} base64_encode[^(]*\([^)]*\) [OR]
+# Block out any script that includes a <script> tag in URL.
+RewriteCond %{QUERY_STRING} (<|%3C)([^s]*s)+cript.*(>|%3E) [NC,OR]
+# Block out any script trying to set a PHP GLOBALS variable via URL.
+RewriteCond %{QUERY_STRING} GLOBALS(=|\[|\%[0-9A-Z]{0,2}) [OR]
+# Block out any script trying to modify a _REQUEST variable via URL.
+RewriteCond %{QUERY_STRING} _REQUEST(=|\[|\%[0-9A-Z]{0,2})
+# Return 403 Forbidden header and show the content of the root homepage
+RewriteRule .* index.php [F]
+#
+## End - Exploits
+
+## Begin - Index
+# If the requested path and file is not /index.php and the request
+# has not already been internally rewritten to the index.php script
+RewriteCond %{REQUEST_URI} !^/index\.php
+# and the requested path and file doesn't directly match a physical file
+RewriteCond %{REQUEST_FILENAME} !-f
+# and the requested path and file doesn't directly match a physical folder
+RewriteCond %{REQUEST_FILENAME} !-d
+# internally rewrite the request to the index.php script
+RewriteRule .* index.php [L]
+## End - Index
+
+## Begin - Security
+# Block all direct access for these folders
+RewriteRule ^(.git|cache|bin|logs|backup)/(.*) error [L]
+# Block access to specific file types for these folders
+RewriteRule ^(system|user|vendor)/(.*)\.(txt|md|html|yaml|php|twig|sh|bat)$ error [L]
+# Block all direct access to .md files:
+RewriteRule \.md$ error [L]
+# Block all direct access to files and folders beginning with a dot
+RewriteRule (^\.|/\.) - [F]
+# Block access to specific files in the root folder
+RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config)$ error [F]
+## End - Security
+
+</IfModule>
+
+# Begin - Prevent Browsing and Set Default Resources
+Options -Indexes
+DirectoryIndex index.php index.html index.htm
+# End - Prevent Browsing and Set Default Resources

+ 42 - 0
index.php

@@ -0,0 +1,42 @@
+<?php
+namespace Grav;
+
+if (version_compare($ver = PHP_VERSION, $req = '5.4.0', '<')) {
+    throw new \RuntimeException(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
+}
+
+// Ensure vendor libraries exist
+$autoload = __DIR__ . '/vendor/autoload.php';
+if (!is_file($autoload)) {
+    throw new \RuntimeException("Please run: <i>bin/grav install</i>");
+}
+
+use Grav\Common\Grav;
+
+// Register the auto-loader.
+$loader = require_once $autoload;
+
+// Set timezone to default, falls back to system if php.ini not set
+date_default_timezone_set(@date_default_timezone_get());
+
+// Set internal encoding if mbstring loaded
+if (!extension_loaded('mbstring')) {
+    throw new \RuntimeException("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
+}
+mb_internal_encoding('UTF-8');
+
+// Get the Grav instance
+$grav = Grav::instance(
+    array(
+        'loader' => $loader
+    )
+);
+
+// Process the page
+try {
+    $grav->process();
+} catch (\Exception $e) {
+    $grav->fireEvent('onFatalException');
+    throw $e;
+}
+

+ 87 - 0
nginx.conf

@@ -0,0 +1,87 @@
+worker_processes 1;
+
+events {
+    worker_connections 1024;
+}
+
+http {
+    include mime.types;
+    default_type application/octet-stream;
+    sendfile on;
+    keepalive_timeout 65;
+
+    server {
+        listen 80;
+        server_name localhost;
+
+        error_page 500 502 503 504 /50x.html;
+        location = /50x.html {
+            root html;
+        }
+
+        location / {
+            root html;
+            index index.php;
+            if (!-e $request_filename){ rewrite ^(.*)$ /index.php last; }
+        }
+
+        # if you want grav in a sub-directory of your main site
+        # (for example, example.com/mygrav) then you need this rewrite:
+        location /mygrav {
+            index index.php;
+            if (!-e $request_filename){ rewrite ^(.*)$ /mygrav/$2 last; }
+            try_files $uri $uri/ /index.php?$args;
+        }
+
+        # if using grav in a sub-directory of your site,
+        # prepend the actual path to each location
+        # for example: /mygrav/images
+        # and: /mygrav/user
+        # and: /mygrav/cache
+        # and so on
+
+        location /images/ {
+            # Serve images as static
+        }
+
+        location /user {
+            rewrite ^/user/accounts/(.*)$ /error redirect;
+            rewrite ^/user/config/(.*)$ /error redirect;
+            rewrite ^/user/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+        }
+
+        location /cache {
+            rewrite ^/cache/(.*) /error redirect;
+        }
+
+        location /bin {
+            rewrite ^/bin/(.*)$ /error redirect;
+        }
+
+        location /backup {
+            rewrite ^/backup/(.*) /error redirect;
+        }
+
+        location /system {
+            rewrite ^/system/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+        }
+
+        location /vendor {
+            rewrite ^/vendor/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+        }
+
+        # Remember to change 127.0.0.1:9000 to the Ip/port
+        # you configured php-cgi.exe to run from
+
+        location ~ \.php$ {
+            try_files $uri =404;
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass 127.0.0.1:9000;
+            fastcgi_index index.php;
+            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            include fastcgi_params;
+        }
+
+    }
+
+}

+ 2 - 0
robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:

BIN
screenshot.png


+ 54 - 0
system/assets/debugger.css

@@ -0,0 +1,54 @@
+div.phpdebugbar {
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.phpdebugbar pre {
+    padding: 1rem;
+}
+
+.phpdebugbar div.phpdebugbar-header > div > * {
+    padding: 5px 15px;
+}
+
+.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
+    padding: 5px 8px;
+}
+
+.phpdebugbar div.phpdebugbar-header, .phpdebugbar a.phpdebugbar-restore-btn {
+    background-image: url(grav.png);
+}
+
+.phpdebugbar a.phpdebugbar-restore-btn {
+    width: 13px;
+}
+
+.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
+    background: #3DB9EC;
+    color: #fff;
+    margin-top: -1px;
+    padding-top: 6px;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar {
+    padding-left: 5px;
+}
+
+.phpdebugbar input[type=text] {
+    padding: 0;
+    display: inline;
+}
+
+.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
+    font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
+    font-size: 12px;
+}
+
+ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
+    text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
+    top: 0;
+}
+
+.phpdebugbar pre, .phpdebugbar code {
+    margin: 0;
+    font-size: 14px;
+}

BIN
system/assets/grav.png


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


BIN
system/assets/responsive-overlays/1x.png


BIN
system/assets/responsive-overlays/2x.png


BIN
system/assets/responsive-overlays/3x.png


BIN
system/assets/responsive-overlays/4x.png


BIN
system/assets/responsive-overlays/unknown.png


+ 110 - 0
system/assets/whoops.css

@@ -0,0 +1,110 @@
+body {
+    background-color: #eee;
+}
+
+body header {
+    background: #349886;
+    border-left: 8px solid #29796B;
+}
+
+body .clipboard {
+    width: 28px;
+    height: 28px;
+    background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcAQMAAABIw03XAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///////9VfPVsAAAAAnRSTlP/AOW3MEoAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAFnRFWHRDcmVhdGlvbiBUaW1lADEwLzE1LzE0xr/LJAAAADhJREFUCJlj+P///wcGBPGDQR5E8OMi2IEEczOIaAQRHSCioQBGHAAR/7AT/z+DiA8MMALVXhABAJf9Sr5aY+UFAAAAAElFTkSuQmCC);
+}
+
+body .exc-title-primary {
+    color: #1C3631;
+    text-shadow: none;
+}
+
+body .exc-title {
+    color: #2F5B52;
+    text-shadow: none;
+}
+
+body .data-table-container label {
+    color: #0082BA;
+}
+
+body .frame {
+    border: 0;
+}
+
+body .frames-container {
+    overflow-y: auto;
+    overflow-x: hidden;
+}
+
+body .active .frame-class {
+    color: #E3D8E9;
+}
+
+body .frame-class {
+    color: #9055AF;
+}
+
+body .frame.active {
+    border: 0;
+    box-shadow: none;
+    background-color: #9055AF;
+}
+
+body .frame:not(.active):hover {
+    background: #e9e9e9;
+}
+
+body .frame-file, body .data-table tbody {
+    font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
+    font-size: 13px;
+}
+
+body .frame-code {
+    background: #305669;
+    border-left: 8px solid #253A47;
+    padding: 1rem;
+}
+
+body .frame-code .frame-file {
+    background: #253A47;
+    color: #eee;
+    text-shadow: none;
+    box-shadow: none;
+    font-family: inherit;
+}
+
+body .frame-code .frame-file strong {
+    color: #fff;
+    font-weight: normal;
+}
+
+body .frame-comments {
+    background: #283E4D;
+
+    box-shadow: none;
+}
+
+body .frame-comments.empty:before {
+    color: #789AAB;
+}
+
+body .details-container {
+    border: 0;
+}
+
+body .details {
+    background-color: #eee;
+    border-left: 8px solid #ddd;
+    padding: 1rem;
+}
+
+body .code-block {
+    background: #2C4454;
+    box-shadow: none;
+    font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
+    font-size: 13px;
+}
+
+body .handler.active {
+    background: #666;
+}

+ 5 - 0
system/blueprints/config/media.yaml

@@ -0,0 +1,5 @@
+title: PLUGIN_ADMIN.MEDIA
+
+form:
+  validation: loose
+  fields:

+ 116 - 0
system/blueprints/config/site.yaml

@@ -0,0 +1,116 @@
+title: PLUGIN_ADMIN.SITE
+form:
+    validation: loose
+    fields:
+
+        content:
+            type: section
+            title: PLUGIN_ADMIN.DEFAULTS
+            underline: true
+
+            fields:
+                title:
+                    type: text
+                    label: PLUGIN_ADMIN.SITE_TITLE
+                    size: large
+                    placeholder: PLUGIN_ADMIN.SITE_TITLE_PLACEHOLDER
+                    help: PLUGIN_ADMIN.SITE_TITLE_HELP
+
+                author.name:
+                    type: text
+                    size: large
+                    label: PLUGIN_ADMIN.DEFAULT_AUTHOR
+                    help: PLUGIN_ADMIN.DEFAULT_AUTHOR_HELP
+
+                author.email:
+                    type: text
+                    size: large
+                    label: PLUGIN_ADMIN.DEFAULT_EMAIL
+                    help: PLUGIN_ADMIN.DEFAULT_EMAIL_HELP
+                    validate:
+                        type: email
+
+                taxonomies:
+                    type: selectize
+                    size: large
+                    label: PLUGIN_ADMIN.TAXONOMY_TYPES
+                    classes: fancy
+                    help: PLUGIN_ADMIN.TAXONOMY_TYPES_HELP
+                    validate:
+                        type: commalist
+
+        summary:
+            type: section
+            title: PLUGIN_ADMIN.PAGE_SUMMARY
+            underline: true
+
+            fields:
+                summary.enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ENABLED
+                    highlight: 1
+                    help: PLUGIN_ADMIN.ENABLED_HELP
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                summary.size:
+                    type: text
+                    size: x-small
+                    label: PLUGIN_ADMIN.SUMMARY_SIZE
+                    help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP
+                    validate:
+                        type: int
+                        min: 0
+                        max: 65536
+
+                summary.format:
+                    type: toggle
+                    label: PLUGIN_ADMIN.FORMAT
+                    classes: fancy
+                    help: PLUGIN_ADMIN.FORMAT_HELP
+                    highlight: short
+                    options:
+                        'short': PLUGIN_ADMIN.SHORT
+                        'long': PLUGIN_ADMIN.LONG
+
+                summary.delimiter:
+                    type: text
+                    size: x-small
+                    label: PLUGIN_ADMIN.DELIMITER
+                    help: PLUGIN_ADMIN.DELIMITER_HELP
+
+        metadata:
+            type: section
+            title: PLUGIN_ADMIN.METADATA
+            underline: true
+
+            fields:
+                metadata:
+                   type: array
+                   label: PLUGIN_ADMIN.METADATA
+                   help: PLUGIN_ADMIN.METADATA_HELP
+                   placeholder_key: PLUGIN_ADMIN.METADATA_KEY
+                   placeholder_value: PLUGIN_ADMIN.METADATA_VALUE
+
+        routes:
+            type: section
+            title: PLUGIN_ADMIN.REDIRECTS_AND_ROUTES
+            underline: true
+
+            fields:
+                redirects:
+                    type: array
+                    label: PLUGIN_ADMIN.CUSTOM_REDIRECTS
+                    help: PLUGIN_ADMIN.CUSTOM_REDIRECTS_HELP
+                    placeholder_key: PLUGIN_ADMIN.CUSTOM_REDIRECTS_PLACEHOLDER_KEY
+                    placeholder_value: PLUGIN_ADMIN.CUSTOM_REDIRECTS_PLACEHOLDER_VALUE
+
+                routes:
+                    type: array
+                    label: PLUGIN_ADMIN.CUSTOM_ROUTES
+                    help: PLUGIN_ADMIN.CUSTOM_ROUTES_HELP
+                    placeholder_key: PLUGIN_ADMIN.CUSTOM_ROUTES_PLACEHOLDER_KEY
+                    placeholder_value: PLUGIN_ADMIN.CUSTOM_ROUTES_PLACEHOLDER_VALUE

+ 7 - 0
system/blueprints/config/streams.yaml

@@ -0,0 +1,7 @@
+title: PLUGIN_ADMIN.FILE_STREAMS
+
+form:
+  validation: loose
+  fields:
+    schemes.xxx:
+      type: array

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

@@ -0,0 +1,793 @@
+title: PLUGIN_ADMIN.SYSTEM
+
+form:
+    validation: loose
+    fields:
+
+        content:
+            type: section
+            title: PLUGIN_ADMIN.CONTENT
+            underline: true
+
+            fields:
+                home.alias:
+                    type: pages
+                    size: medium
+                    classes: fancy
+                    label: PLUGIN_ADMIN.HOME_PAGE
+                    show_all: false
+                    show_modular: false
+                    show_root: false
+                    help: PLUGIN_ADMIN.HOME_PAGE_HELP
+
+                pages.theme:
+                    type: themeselect
+                    classes: fancy
+                    selectize: true
+                    size: medium
+                    label: PLUGIN_ADMIN.DEFAULT_THEME
+                    help: PLUGIN_ADMIN.DEFAULT_THEME_HELP
+
+                pages.process:
+                    type: checkboxes
+                    label: PLUGIN_ADMIN.PROCESS
+                    help: PLUGIN_ADMIN.PROCESS_HELP
+                    default: [markdown: true, twig: true]
+                    options:
+                        markdown: Markdown
+                        twig: Twig
+                    use: keys
+
+                timezone:
+                    type: select
+                    label: PLUGIN_ADMIN.TIMEZONE
+                    size: medium
+                    classes: fancy
+                    help: PLUGIN_ADMIN.TIMEZONE_HELP
+                    '@data-options': '\Grav\Common\Utils::timezones'
+                    default: ''
+                    options:
+                        '': 'Default (Server Timezone)'
+
+                pages.dateformat.default:
+                    type: select
+                    size: medium
+                    selectize:
+                        create: true
+                    label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
+                    help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
+                    placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
+                    '@data-options': '\Grav\Common\Utils::dateFormats'
+                    options:
+                        "": Auto Guess or Enter Custom
+                    validate:
+                        type: string
+
+                pages.dateformat.short:
+                    type: dateformat
+                    size: medium
+                    classes: fancy
+                    label: PLUGIN_ADMIN.SHORT_DATE_FORMAT
+                    help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP
+                    default: "jS M Y"
+                    options:
+                        "F jS \\a\\t g:ia": Date1
+                        "l jS \\of F g:i A": Date2
+                        "D, m M Y G:i:s": Date3
+                        "d-m-y G:i": Date4
+                        "jS M Y": Date5
+
+                pages.dateformat.long:
+                    type: dateformat
+                    size: medium
+                    classes: fancy
+                    label: PLUGIN_ADMIN.LONG_DATE_FORMAT
+                    help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP
+                    options:
+                        "F jS \\a\\t g:ia": Date1
+                        "l jS \\of F g:i A": Date2
+                        "D, m M Y G:i:s": Date3
+                        "d-m-y G:i": Date4
+                        "jS M Y": Date5
+
+                pages.order.by:
+                    type: select
+                    size: long
+                    classes: fancy
+                    label: PLUGIN_ADMIN.DEFAULT_ORDERING
+                    help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP
+                    options:
+                        default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT
+                        folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER
+                        title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE
+                        date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE
+
+                pages.order.dir:
+                    type: toggle
+                    label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION
+                    highlight: asc
+                    default: desc
+                    help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP
+                    options:
+                        asc: PLUGIN_ADMIN.ASCENDING
+                        desc: PLUGIN_ADMIN.DESCENDING
+
+                pages.list.count:
+                    type: text
+                    size: x-small
+                    label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT
+                    help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP
+                    validate:
+                        type: number
+                        min: 1
+
+                pages.publish_dates:
+                    type: toggle
+                    label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING
+                    help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                pages.events:
+                     type: checkboxes
+                     label: PLUGIN_ADMIN.EVENTS
+                     help: PLUGIN_ADMIN.EVENTS_HELP
+                     default: [page: true, twig: true]
+                     options:
+                         page: Page Events
+                         twig: Twig Events
+                     use: keys
+
+                pages.redirect_default_route:
+                    type: toggle
+                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
+                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                pages.redirect_default_code:
+                    type: select
+                    size: medium
+                    classes: fancy
+                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
+                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
+                    options:
+                        301: 301 - Permanent
+                        303: 303 - Other
+                        307: 307 - Temporary
+
+                pages.redirect_trailing_slash:
+                    type: toggle
+                    label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
+                    help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                pages.ignore_hidden:
+                    type: toggle
+                    label: PLUGIN_ADMIN.IGNORE_HIDDEN
+                    help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                pages.ignore_files:
+                    type: selectize
+                    size: large
+                    label: PLUGIN_ADMIN.IGNORE_FILES
+                    help: PLUGIN_ADMIN.IGNORE_FILES_HELP
+                    classes: fancy
+                    validate:
+                        type: commalist
+
+                pages.ignore_folders:
+                    type: selectize
+                    size: large
+                    label: PLUGIN_ADMIN.IGNORE_FOLDERS
+                    help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
+                    classes: fancy
+                    validate:
+                        type: commalist
+
+                pages.url_taxonomy_filters:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS
+                    help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                pages.fallback_types:
+                    type: selectize
+                    size: large
+                    label: PLUGIN_ADMIN.FALLBACK_TYPES
+                    help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP
+                    classes: fancy
+                    validate:
+                        type: commalist
+
+        languages:
+            type: section
+            title: PLUGIN_ADMIN.LANGUAGES
+            underline: true
+
+            fields:
+
+                languages.supported:
+                    type: selectize
+                    size: large
+                    label: PLUGIN_ADMIN.SUPPORTED
+                    help: PLUGIN_ADMIN.SUPPORTED_HELP
+                    classes: fancy
+                    validate:
+                        type: commalist
+
+                languages.include_default_lang:
+                    type: toggle
+                    label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG
+                    help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+
+                languages.translations:
+                    type: toggle
+                    label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED
+                    help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                languages.translations_fallback:
+                    type: toggle
+                    label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK
+                    help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                languages.session_store_active:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION
+                    help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                languages.http_accept_language:
+                    type: toggle
+                    label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
+                    help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                languages.override_locale:
+                    type: toggle
+                    label: PLUGIN_ADMIN.OVERRIDE_LOCALE
+                    help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        http_headers:
+            type: section
+            title: PLUGIN_ADMIN.HTTP_HEADERS
+            underline: true
+
+            fields:
+                pages.expires:
+                    type: text
+                    size: small
+                    label: PLUGIN_ADMIN.EXPIRES
+                    help: PLUGIN_ADMIN.EXPIRES_HELP
+                    validate:
+                        type: number
+                        min: 1
+                pages.last_modified:
+                    type: toggle
+                    label: PLUGIN_ADMIN.LAST_MODIFIED
+                    help: PLUGIN_ADMIN.LAST_MODIFIED_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+                pages.etag:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ETAG
+                    help: PLUGIN_ADMIN.ETAG_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+                pages.vary_accept_encoding:
+                    type: toggle
+                    label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING
+                    help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        markdown:
+            type: section
+            title: Markdown
+            underline: true
+
+            fields:
+                pages.markdown.extra:
+                    type: toggle
+                    label: Markdown extra
+                    help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+                pages.markdown.auto_line_breaks:
+                    type: toggle
+                    label: PLUGIN_ADMIN.AUTO_LINE_BREAKS
+                    help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+                pages.markdown.auto_url_links:
+                    type: toggle
+                    label: PLUGIN_ADMIN.AUTO_URL_LINKS
+                    help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+                pages.markdown.escape_markup:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ESCAPE_MARKUP
+                    help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        caching:
+            type: section
+            title: PLUGIN_ADMIN.CACHING
+            underline: true
+
+            fields:
+                cache.enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CACHING
+                    help: PLUGIN_ADMIN.CACHING_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                cache.check.method:
+                    type: select
+                    size: small
+                    classes: fancy
+                    label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
+                    help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
+                    options:
+                        file: File
+                        folder: Folder
+                        none: None
+
+                cache.driver:
+                    type: select
+                    size: small
+                    classes: fancy
+                    label: PLUGIN_ADMIN.CACHE_DRIVER
+                    help: PLUGIN_ADMIN.CACHE_DRIVER_HELP
+                    options:
+                        auto: Auto detect
+                        file: File
+                        apc: APC
+                        xcache: XCache
+                        memcache: MemCache
+                        wincache: WinCache
+
+                cache.prefix:
+                    type: text
+                    size: x-small
+                    label: PLUGIN_ADMIN.CACHE_PREFIX
+                    help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
+                    placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
+
+                cache.lifetime:
+                    type: text
+                    size: small
+                    label: PLUGIN_ADMIN.LIFETIME
+                    help: PLUGIN_ADMIN.LIFETIME_HELP
+                    validate:
+                        type: number
+
+                cache.gzip:
+                    type: toggle
+                    label: PLUGIN_ADMIN.GZIP_COMPRESSION
+                    help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+
+        twig:
+            type: section
+            title: PLUGIN_ADMIN.TWIG_TEMPLATING
+            underline: true
+
+            fields:
+                twig.cache:
+                    type: toggle
+                    label: PLUGIN_ADMIN.TWIG_CACHING
+                    help: PLUGIN_ADMIN.TWIG_CACHING_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                twig.debug:
+                    type: toggle
+                    label: PLUGIN_ADMIN.TWIG_DEBUG
+                    help: PLUGIN_ADMIN.TWIG_DEBUG_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                twig.auto_reload:
+                    type: toggle
+                    label: PLUGIN_ADMIN.DETECT_CHANGES
+                    help: PLUGIN_ADMIN.DETECT_CHANGES_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                twig.autoescape:
+                    type: toggle
+                    label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES
+                    help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        assets:
+            type: section
+            title: PLUGIN_ADMIN.ASSETS
+            underline: true
+
+            fields:
+                assets.css_pipeline:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CSS_PIPELINE
+                    help: PLUGIN_ADMIN.CSS_PIPELINE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                       type: bool
+
+                assets.css_minify:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CSS_MINIFY
+                    help: PLUGIN_ADMIN.CSS_MINIFY_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                       type: bool
+
+                assets.css_minify_windows:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE
+                    help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                assets.css_rewrite:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CSS_REWRITE
+                    help: PLUGIN_ADMIN.CSS_REWRITE_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                assets.js_pipeline:
+                    type: toggle
+                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
+                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                assets.js_minify:
+                    type: toggle
+                    label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY
+                    help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                assets.enable_asset_timestamp:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
+                    help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                assets.collections:
+                    type: array
+                    label: PLUGIN_ADMIN.COLLECTIONS
+                    placeholder_key: collection_name
+                    placeholder_value: collection_path
+
+        errors:
+            type: section
+            title: PLUGIN_ADMIN.ERROR_HANDLER
+            underline: true
+
+            fields:
+                errors.display:
+                    type: toggle
+                    label: PLUGIN_ADMIN.DISPLAY_ERRORS
+                    help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                errors.log:
+                    type: toggle
+                    label: PLUGIN_ADMIN.LOG_ERRORS
+                    help: PLUGIN_ADMIN.LOG_ERRORS_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        debugger:
+            type: section
+            title: PLUGIN_ADMIN.DEBUGGER
+            underline: true
+
+            fields:
+                debugger.enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.DEBUGGER
+                    help: PLUGIN_ADMIN.DEBUGGER_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                debugger.shutdown.close_connection:
+                    type: toggle
+                    label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION
+                    help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        media:
+            type: section
+            title: PLUGIN_ADMIN.MEDIA
+            underline: true
+
+            fields:
+                images.default_image_quality:
+                    type: text
+                    label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY
+                    help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP
+                    classes: x-small
+                    validate:
+                        type: number
+                        min: 1
+                        max: 100
+
+                images.cache_all:
+                    type: toggle
+                    label: PLUGIN_ADMIN.CACHE_ALL
+                    help: PLUGIN_ADMIN.CACHE_ALL_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                images.debug:
+                    type: toggle
+                    label: PLUGIN_ADMIN.IMAGES_DEBUG
+                    help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                media.upload_limit:
+                    type: text
+                    label: PLUGIN_ADMIN.UPLOAD_LIMIT
+                    help: PLUGIN_ADMIN.UPLOAD_LIMIT_HELP
+                    classes: small
+                    validate:
+                        type: number
+
+                media.enable_media_timestamp:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP
+                    help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP
+                    highlight: 0
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+        session:
+            type: section
+            title: PLUGIN_ADMIN.SESSION
+            underline: true
+
+            fields:
+                session.enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ENABLED
+                    help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
+                    highlight: 1
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                session.timeout:
+                    type: text
+                    size: small
+                    label: PLUGIN_ADMIN.TIMEOUT
+                    help: PLUGIN_ADMIN.TIMEOUT_HELP
+                    validate:
+                        type: number
+                        min: 1
+
+                session.name:
+                    type: text
+                    size: small
+                    label: PLUGIN_ADMIN.NAME
+                    help: PLUGIN_ADMIN.SESSION_NAME_HELP
+
+
+        advanced:
+            type: section
+            title: PLUGIN_ADMIN.ADVANCED
+            underline: true
+
+            fields:
+                wrapped_site:
+                    type: toggle
+                    label: PLUGIN_ADMIN.WRAPPED_SITE
+                    highlight: 0
+                    help: PLUGIN_ADMIN.WRAPPED_SITE_HELP
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                absolute_urls:
+                    type: toggle
+                    label: PLUGIN_ADMIN.ABSOLUTE_URLS
+                    highlight: 0
+                    help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP
+                    options:
+                        1: PLUGIN_ADMIN.YES
+                        0: PLUGIN_ADMIN.NO
+                    validate:
+                        type: bool
+
+                param_sep:
+                    type: select
+                    size: medium
+                    label: PLUGIN_ADMIN.PARAMETER_SEPARATOR
+                    classes: fancy
+                    help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP
+                    default: ''
+                    options:
+                        ':': ': (default)'
+                        ';': '; (for Apache running on Windows)'

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

@@ -0,0 +1,276 @@
+title: PLUGIN_ADMIN.DEFAULT
+
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+
+  fields:
+
+    tabs:
+      type: tabs
+      active: 1
+
+      fields:
+        content:
+          type: tab
+          title: PLUGIN_ADMIN.CONTENT
+
+          fields:
+            header.title:
+              type: text
+              autofocus: true
+              style: vertical
+              label: PLUGIN_ADMIN.TITLE
+
+            content:
+                type: markdown
+                label: PLUGIN_ADMIN.CONTENT
+                validate:
+                  type: textarea
+
+            uploads:
+              type: pagemedia
+              label: PLUGIN_ADMIN.PAGE_MEDIA
+
+        options:
+          type: tab
+          title: PLUGIN_ADMIN.OPTIONS
+
+          fields:
+
+            publishing:
+              type: section
+              title: Publishing
+              underline: true
+
+              fields:
+                header.published:
+                  type: toggle
+                  toggleable: true
+                  label: PLUGIN_ADMIN.PUBLISHED
+                  help: PLUGIN_ADMIN.PUBLISHED_HELP
+                  highlight: 1
+                  size: medium
+                  options:
+                    1: PLUGIN_ADMIN.YES
+                    0: PLUGIN_ADMIN.NO
+                  validate:
+                    type: bool
+
+                header.date:
+                  type: datetime
+                  label: PLUGIN_ADMIN.DATE
+                  toggleable: true
+                  help: PLUGIN_ADMIN.DATE_HELP
+
+                header.publish_date:
+                  type: datetime
+                  label: PLUGIN_ADMIN.PUBLISHED_DATE
+                  toggleable: true
+                  help: PLUGIN_ADMIN.PUBLISHED_DATE_HELP
+
+                header.unpublish_date:
+                  type: datetime
+                  label: PLUGIN_ADMIN.UNPUBLISHED_DATE
+                  toggleable: true
+                  help: PLUGIN_ADMIN.UNPUBLISHED_DATE_HELP
+
+                header.metadata:
+                  toggleable: true
+                  type: array
+                  label: PLUGIN_ADMIN.METADATA
+                  help: PLUGIN_ADMIN.METADATA_HELP
+                  placeholder_key: PLUGIN_ADMIN.METADATA_KEY
+                  placeholder_value: PLUGIN_ADMIN.METADATA_VALUE
+
+
+            taxonomies:
+              type: section
+              title: PLUGIN_ADMIN.TAXONOMIES
+              underline: true
+
+              fields:
+                header.taxonomy:
+                  type: taxonomy
+                  label: PLUGIN_ADMIN.TAXONOMY
+                  multiple: true
+                  validate:
+                    type: array
+
+        advanced:
+          type: tab
+          title: PLUGIN_ADMIN.ADVANCED
+
+          fields:
+            columns:
+              type: columns
+              fields:
+                column1:
+                  type: column
+                  fields:
+
+                    settings:
+                      type: section
+                      title: PLUGIN_ADMIN.SETTINGS
+                      underline: true
+
+                    ordering:
+                      type: toggle
+                      label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
+                      help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
+                      highlight: 1
+                      options:
+                        1: PLUGIN_ADMIN.ENABLED
+                        0: PLUGIN_ADMIN.DISABLED
+                      validate:
+                        type: bool
+
+                    folder:
+                      type: text
+                      label: PLUGIN_ADMIN.FOLDER_NAME
+                      validate:
+                        type: slug
+
+                    route:
+                      type: select
+                      label: PLUGIN_ADMIN.PARENT
+                      classes: fancy
+                      '@data-options': '\Grav\Common\Page\Pages::parents'
+                      '@data-default': '\Grav\Plugin\admin::route'
+                      options:
+                        '/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
+
+                    name:
+                      type: select
+                      classes: fancy
+                      label: PLUGIN_ADMIN.PAGE_FILE
+                      help: PLUGIN_ADMIN.PAGE_FILE_HELP
+                      default: default
+                      '@data-options': '\Grav\Common\Page\Pages::pageTypes'
+
+                    header.body_classes:
+                      type: text
+                      label: PLUGIN_ADMIN.BODY_CLASSES
+
+
+                column2:
+                  type: column
+
+                  fields:
+                    order_title:
+                      type: section
+                      title: PLUGIN_ADMIN.ORDERING
+                      underline: true
+
+                    order:
+                      type: order
+                      label: PLUGIN_ADMIN.PAGE_ORDER
+                      sitemap:
+
+            overrides:
+              type: section
+              title: PLUGIN_ADMIN.OVERRIDES
+              underline: true
+
+              fields:
+
+                header.menu:
+                  type: text
+                  label: PLUGIN_ADMIN.MENU
+                  toggleable: true
+                  help: PLUGIN_ADMIN.MENU_HELP
+
+                header.slug:
+                  type: text
+                  label: PLUGIN_ADMIN.SLUG
+                  toggleable: true
+                  help: PLUGIN_ADMIN.SLUG_HELP
+                  validate:
+                    message: PLUGIN_ADMIN.SLUG_VALIDATE_MESSAGE
+                    rule: slug
+
+                header.redirect:
+                  type: text
+                  label: PLUGIN_ADMIN.REDIRECT
+                  toggleable: true
+                  help: PLUGIN_ADMIN.REDIRECT_HELP
+
+                header.process:
+                  type: checkboxes
+                  label: PLUGIN_ADMIN.PROCESS
+                  toggleable: true
+                  '@config-default': system.pages.process
+                  default:
+                    markdown: true
+                    twig: false
+                  options:
+                    markdown: Markdown
+                    twig: Twig
+                  use: keys
+
+                header.child_type:
+                  type: select
+                  toggleable: true
+                  label: PLUGIN_ADMIN.DEFAULT_CHILD_TYPE
+                  default: default
+                  placeholder: PLUGIN_ADMIN.USE_GLOBAL
+                  '@data-options': '\Grav\Common\Page\Pages::types'
+
+                header.routable:
+                  type: toggle
+                  toggleable: true
+                  label: PLUGIN_ADMIN.ROUTABLE
+                  help: PLUGIN_ADMIN.ROUTABLE_HELP
+                  highlight: 1
+                  options:
+                    1: PLUGIN_ADMIN.ENABLED
+                    0: PLUGIN_ADMIN.DISABLED
+                  validate:
+                    type: bool
+
+                header.cache_enable:
+                  type: toggle
+                  toggleable: true
+                  label: PLUGIN_ADMIN.CACHING
+                  highlight: 1
+                  options:
+                    1: PLUGIN_ADMIN.ENABLED
+                    0: PLUGIN_ADMIN.DISABLED
+                  validate:
+                    type: bool
+
+                header.visible:
+                  type: toggle
+                  toggleable: true
+                  label: PLUGIN_ADMIN.VISIBLE
+                  help: PLUGIN_ADMIN.VISIBLE_HELP
+                  highlight: 1
+                  options:
+                    1: PLUGIN_ADMIN.ENABLED
+                    0: PLUGIN_ADMIN.DISABLED
+                  validate:
+                    type: bool
+
+                header.template:
+                  type: select
+                  toggleable: true
+                  classes: fancy
+                  label: PLUGIN_ADMIN.DISPLAY_TEMPLATE
+                  default: default
+                  '@data-options': '\Grav\Common\Page\Pages::types'
+
+                header.order_by:
+                  type: hidden
+
+                header.order_manual:
+                  type: hidden
+                  validate:
+                    type: commalist
+
+                blueprint:
+                  type: blueprint

+ 47 - 0
system/blueprints/pages/modular.yaml

@@ -0,0 +1,47 @@
+title: PLUGIN_ADMIN.MODULAR
+@extends:
+    type: default
+    context: blueprints://pages
+
+form:
+  fields:
+    tabs:
+      type: tabs
+      active: 1
+
+      fields:
+        content:
+          fields:
+
+            header.content.items:
+              type: select
+              label: PLUGIN_ADMIN.ITEMS
+              default: '@self.modular'
+              options:
+                '@self.modular': Children
+
+            header.content.order.by:
+              type: select
+              label: PLUGIN_ADMIN.ORDER_BY
+              default: date
+              options:
+                folder: PLUGIN_ADMIN.FOLDER
+                title: PLUGIN_ADMIN.TITLE
+                date: PLUGIN_ADMIN.DATE
+                default: PLUGIN_ADMIN.DEFAULT
+
+            header.content.order.dir:
+              type: select
+              label: PLUGIN_ADMIN.ORDER
+              default: desc
+              options:
+                asc: PLUGIN_ADMIN.ASCENDING
+                desc: PLUGIN_ADMIN.DESCENDING
+
+            header.process:
+              type: ignore
+            content:
+              type: ignore
+            uploads:
+              type: ignore
+

+ 56 - 0
system/blueprints/pages/modular_new.yaml

@@ -0,0 +1,56 @@
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+  fields:
+
+    section:
+        type: section
+        title: PLUGIN_ADMIN.ADD_MODULAR_CONTENT
+
+    title:
+      type: text
+      label: PLUGIN_ADMIN.PAGE_TITLE
+      validate:
+        required: true
+
+    folder:
+      type: text
+      label: PLUGIN_ADMIN.FOLDER_NAME
+      validate:
+        type: slug
+        required: true
+
+    route:
+      type: select
+      label: PLUGIN_ADMIN.PAGE
+      classes: fancy
+      '@data-options': '\Grav\Common\Page\Pages::parents'
+      '@data-default': '\Grav\Plugin\admin::route'
+      options:
+        '': PLUGIN_ADMIN.DEFAULT_OPTION_SELECT
+      validate:
+        required: true
+
+    name:
+      type: select
+      classes: fancy
+      label: PLUGIN_ADMIN.MODULAR_TEMPLATE
+      help: PLUGIN_ADMIN.PAGE_FILE_HELP
+      default: default
+      '@data-options': '\Grav\Common\Page\Pages::modularTypes'
+      validate:
+        required: true
+
+    modular:
+      type: hidden
+      default: 1
+      validate:
+        type: bool
+
+    blueprint:
+      type: blueprint

+ 97 - 0
system/blueprints/pages/modular_raw.yaml

@@ -0,0 +1,97 @@
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+  fields:
+
+    tabs:
+      type: tabs
+      active: 1
+
+      fields:
+        content:
+          type: tab
+          title: PLUGIN_ADMIN.CONTENT
+
+          fields:
+            frontmatter:
+              type: frontmatter
+              label: PLUGIN_ADMIN.FRONTMATTER
+
+
+            content:
+              type: markdown
+              label: PLUGIN_ADMIN.CONTENT
+
+            uploads:
+              type: pagemedia
+              label: PLUGIN_ADMIN.PAGE_MEDIA
+
+
+        options:
+          type: tab
+          title: PLUGIN_ADMIN.OPTIONS
+
+          fields:
+
+            columns:
+              type: columns
+
+              fields:
+                column1:
+                  type: column
+
+                  fields:
+
+                    ordering:
+                      type: toggle
+                      label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
+                      help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
+                      highlight: 1
+                      options:
+                        1: PLUGIN_ADMIN.ENABLED
+                        0: PLUGIN_ADMIN.DISABLED
+                      validate:
+                        type: bool
+
+                    folder:
+                      type: text
+                      label: PLUGIN_ADMIN.FILENAME
+                      validate:
+                        type: slug
+                        required: true
+
+                    route:
+                      type: select
+                      label: PLUGIN_ADMIN.PARENT
+                      classes: fancy
+                      '@data-options': '\Grav\Common\Page\Pages::parents'
+                      '@data-default': '\Grav\Plugin\admin::route'
+                      options:
+                        '': PLUGIN_ADMIN.DEFAULT_OPTION_SELECT
+                      validate:
+                        required: true
+
+                    name:
+                      type: select
+                      classes: fancy
+                      label: PLUGIN_ADMIN.MODULAR_TEMPLATE
+                      default: default
+                      '@data-options': '\Grav\Common\Page\Pages::modularTypes'
+                      validate:
+                        required: true
+
+                column2:
+                    type: column
+
+                    fields:
+                      order:
+                        type: order
+                        label: PLUGIN_ADMIN.ORDERING
+
+            blueprint:
+              type: blueprint

+ 17 - 0
system/blueprints/pages/move.yaml

@@ -0,0 +1,17 @@
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+  fields:
+    route:
+      type: select
+      label: PLUGIN_ADMIN.PARENT
+      classes: fancy
+      '@data-options': '\Grav\Common\Page\Pages::parents'
+      '@data-default': '\Grav\Plugin\admin::route'
+      options:
+        '/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT

+ 66 - 0
system/blueprints/pages/new.yaml

@@ -0,0 +1,66 @@
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+  fields:
+
+    section:
+        type: section
+        title: PLUGIN_ADMIN.ADD_PAGE
+
+    title:
+      type: text
+      label: PLUGIN_ADMIN.PAGE_TITLE
+      help: PLUGIN_ADMIN.PAGE_TITLE_HELP
+      validate:
+        required: true
+
+    folder:
+      type: text
+      label: PLUGIN_ADMIN.FOLDER_NAME
+      help: PLUGIN_ADMIN.FOLDER_NAME_HELP
+      validate:
+        type: slug
+        required: true
+
+    route:
+      type: select
+      label: PLUGIN_ADMIN.PARENT_PAGE
+      classes: fancy
+      '@data-options': '\Grav\Common\Page\Pages::parents'
+      '@data-default': '\Grav\Plugin\admin::getLastPageRoute'
+      options:
+        '/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
+      validate:
+        required: true
+
+    name:
+      type: select
+      classes: fancy
+      label: PLUGIN_ADMIN.PAGE_FILE
+      help: PLUGIN_ADMIN.PAGE_FILE_HELP
+      '@data-options': '\Grav\Common\Page\Pages::types'
+      '@data-default': '\Grav\Plugin\admin::getLastPageName'
+      validate:
+        required: true
+
+    visible:
+      type: toggle
+      label: PLUGIN_ADMIN.VISIBLE
+      help: PLUGIN_ADMIN.VISIBLE_HELP
+      highlight: ''
+      default: ''
+      options:
+        '': Auto
+        1: PLUGIN_ADMIN.YES
+        0: PLUGIN_ADMIN.NO
+      validate:
+        type: bool
+        required: true
+
+    blueprint:
+      type: blueprint

+ 96 - 0
system/blueprints/pages/raw.yaml

@@ -0,0 +1,96 @@
+rules:
+  slug:
+    pattern: "[a-z][a-z0-9_\-]+"
+    min: 2
+    max: 80
+
+form:
+  validation: loose
+  fields:
+
+    tabs:
+      type: tabs
+      active: 1
+
+      fields:
+        content:
+          type: tab
+          title: PLUGIN_ADMIN.CONTENT
+
+          fields:
+            frontmatter:
+              type: frontmatter
+              label: PLUGIN_ADMIN.FRONTMATTER
+              autofocus: true
+
+            content:
+              type: markdown
+              label: PLUGIN_ADMIN.CONTENT
+
+            uploads:
+              type: pagemedia
+              label: PLUGIN_ADMIN.PAGE_MEDIA
+
+        options:
+          type: tab
+          title: PLUGIN_ADMIN.OPTIONS
+
+          fields:
+
+            columns:
+              type: columns
+
+              fields:
+                column1:
+                  type: column
+
+                  fields:
+
+                    ordering:
+                      type: toggle
+                      label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
+                      help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
+                      highlight: 1
+                      options:
+                        1: PLUGIN_ADMIN.ENABLED
+                        0: PLUGIN_ADMIN.DISABLED
+                      validate:
+                        type: bool
+
+                    folder:
+                      type: text
+                      label: PLUGIN_ADMIN.FOLDER_NAME
+                      help: PLUGIN_ADMIN.FOLDER_NAME_HELP
+                      validate:
+                        type: slug
+                        required: true
+
+                    route:
+                      type: select
+                      label: PLUGIN_ADMIN.PARENT
+                      classes: fancy
+                      '@data-options': '\Grav\Common\Page\Pages::parents'
+                      '@data-default': '\Grav\Plugin\admin::route'
+                      options:
+                        '/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
+
+                    name:
+                      type: select
+                      classes: fancy
+                      label: PLUGIN_ADMIN.DISPLAY_TEMPLATE
+                      help: PLUGIN_ADMIN.DISPLAY_TEMPLATE_HELP
+                      default: default
+                      '@data-options': '\Grav\Common\Page\Pages::types'
+                      validate:
+                        required: true
+
+                column2:
+                    type: column
+
+                    fields:
+                      order:
+                        type: order
+                        label: PLUGIN_ADMIN.ORDERING
+
+            blueprint:
+              type: blueprint

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

@@ -0,0 +1,56 @@
+title: Site
+form:
+    validation: loose
+    fields:
+
+        content:
+            type: section
+            title: PLUGIN_ADMIN.ACCOUNT
+
+            fields:
+                username:
+                    type: text
+                    size: large
+                    label: PLUGIN_ADMIN.USERNAME
+                    disabled: true
+                    readonly: true
+
+                email:
+                    type: email
+                    size: large
+                    label: PLUGIN_ADMIN.EMAIL
+                    validate:
+                      type: email
+                      message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
+                      required: true
+
+                password:
+                    type: password
+                    size: large
+                    label: PLUGIN_ADMIN.PASSWORD
+                    validate:
+                      required: true
+                      message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
+                      pattern: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
+
+                fullname:
+                    type: text
+                    size: large
+                    label: PLUGIN_ADMIN.FULL_NAME
+                    validate:
+                      required: true
+
+                title:
+                    type: text
+                    size: large
+                    label: PLUGIN_ADMIN.TITLE
+
+                language:
+                    type: select
+                    label: PLUGIN_ADMIN.LANGUAGE
+                    size: medium
+                    classes: fancy
+                    '@data-options': '\Grav\Plugin\admin::adminLanguages'
+                    default: 'en'
+                    help: PLUGIN_ADMIN.LANGUAGE_HELP
+

+ 16 - 0
system/blueprints/user/account_new.yaml

@@ -0,0 +1,16 @@
+title: PLUGIN_ADMIN.ADD_ACCOUNT
+
+form:
+  validation: loose
+  fields:
+
+    content:
+      type: section
+      title: PLUGIN_ADMIN.ADD_ACCOUNT
+
+    username:
+      type: text
+      label: PLUGIN_ADMIN.USERNAME
+      help: PLUGIN_ADMIN.USERNAME_HELP
+      validate:
+        required: true

+ 190 - 0
system/config/media.yaml

@@ -0,0 +1,190 @@
+defaults:
+  type: file
+  thumb: media/thumb.png
+  mime: application/octet-stream
+  image:
+    filters:
+      default:
+        - enableProgressive
+
+jpg:
+  type: image
+  thumb: media/thumb-jpg.png
+  mime: image/jpeg
+jpe:
+  type: image
+  thumb: media/thumb-jpg.png
+  mime: image/jpeg
+jpeg:
+  type: image
+  thumb: media/thumb-jpeg.png
+  mime: image/jpeg
+png:
+  type: image
+  thumb: media/thumb-png.png
+  mime: image/png
+gif:
+  type: animated
+  thumb: media/thumb-gif.png
+  mime: image/gif
+
+svg:
+  type: vector
+  thumb: media/thumb-gif.png
+  mime: image/svg+xml
+
+mp4:
+  type: video
+  thumb: media/thumb-mp4.png
+  mime: video/mp4
+mov:
+  type: video
+  thumb: media/thumb-mov.png
+  mime: video/quicktime
+m4v:
+  type: video
+  thumb: media/thumb-m4v.png
+  mime: video/x-m4v
+swf:
+  type: video
+  thumb: media/thumb-swf.png
+  mime: video/x-flv
+flv:
+  type: video
+  thumb: media/thumb-flv.png
+  mime: video/x-flv
+
+mp3:
+  type: audio
+  thumb: media/thumb-mp3.png
+  mime: audio/mp3
+ogg:
+  type: audio
+  thumb: media/thumb-ogg.png
+  mime: audio/ogg
+wma:
+  type: audio
+  thumb: media/thumb-wma.png
+  mime: audio/wma
+m4a:
+  type: audio
+  thumb: media/thumb-m4a.png
+  mime: audio/m4a
+wav:
+  type: audio
+  thumb: media/thumb-wav.png
+  mime: audio/wav
+aiff:
+  type: audio
+  mime: audio/aiff
+aif:
+  type: audio
+  mime: audio/aif
+
+txt:
+  type: file
+  thumb: media/thumb-txt.png
+  mime: text/plain
+xml:
+  type: file
+  thumb: media/thumb-xml.png
+  mime: application/xml
+doc:
+  type: file
+  thumb: media/thumb-doc.png
+  mime: application/msword
+docx:
+  type: file
+  mime: application/msword
+xls:
+  type: file
+  mime: application/vnd.ms-excel
+xlt:
+  type: file
+  mime: application/vnd.ms-excel
+xlm:
+  type: file
+  mime: application/vnd.ms-excel
+xld:
+  type: file
+  mime: application/vnd.ms-excel
+xla:
+  type: file
+  mime: application/vnd.ms-excel
+xlc:
+  type: file
+  mime: application/vnd.ms-excel
+xlw:
+  type: file
+  mime: application/vnd.ms-excel
+xll:
+  type: file
+  mime: application/vnd.ms-excel
+ppt:
+  type: file
+  mime: application/vnd.ms-powerpoint
+pps:
+  type: file
+  mime: application/vnd.ms-powerpoint
+rtf:
+  type: file
+  mime: application/rtf
+
+bmp:
+  type: file
+  mime: image/bmp
+tiff:
+  type: file
+  mime: image/tiff
+mpeg:
+  type: file
+  mime: video/mpeg
+mpg:
+  type: file
+  mime: video/mpeg
+mpe:
+  type: file
+  mime: video/mpeg
+avi:
+  type: file
+  mime: video/msvideo
+wmv:
+  type: file
+  mime: video/x-ms-wmv
+
+html:
+  type: file
+  thumb: media/thumb-html.png
+  mime: text/html
+htm:
+  type: file
+  thumb: media/thumb-html.png
+  mime: text/html
+pdf:
+  type: file
+  thumb: media/thumb-pdf.png
+  mime: application/pdf
+zip:
+  type: file
+  thumb: media/thumb-zip.png
+  mime: application/zip
+gz:
+  type: file
+  thumb: media/thumb-gz.png
+  mime: application/gzip
+tar:
+  type: file
+  mime: application/x-tar
+css:
+  type: file
+  thumb: media/thumb-css.png
+  mime: text/css
+js:
+  type: file
+  thumb: media/thumb-js.png
+  mime: application/javascript
+json:
+  type: file
+  thumb: media/thumb-json.png
+  mime: application/json
+

+ 34 - 0
system/config/site.yaml

@@ -0,0 +1,34 @@
+title: Grav                                 # Name of the site
+
+author:
+  name: John Appleseed                      # Default author name
+  email: 'john@email.com'                   # Default author email
+
+taxonomies: [category,tag]                  # Arbitrary list of taxonomy types
+
+metadata:
+  description: 'My Grav Site'               # Site description
+
+summary:
+  enabled: true                             # enable or disable summary of page
+  format: short                             # long = summary delimiter will be ignored; short = use the first occurrence of delimiter or size
+  size: 300                                 # Maximum length of summary (characters)
+  delimiter: ===                            # The summary delimiter
+
+redirects:
+  /redirect-test: /                         # Redirect test goes to home page
+  /old/(.*): /new/$1                        # Would redirect /old/my-page to /new/my-page
+
+routes:
+  /something/else: '/blog/sample-3'         # Alias for /blog/sample-3
+  /new/(.*): '/blog/$1'                     # Regex any /new/my-page URL to /blog/my-page Route
+
+blog:
+  route: '/blog'                            # Custom value added (accessible via system.blog.route)
+
+#menu:                                      # Sample Menu Example
+#    - text: Source
+#      icon: github
+#      url: https://github.com/getgrav/grav
+#    - icon: twitter
+#      url: http://twitter.com/getgrav

+ 21 - 0
system/config/streams.yaml

@@ -0,0 +1,21 @@
+schemes:
+  asset:
+    type: ReadOnlyStream
+    paths:
+      - assets
+
+  image:
+    type: ReadOnlyStream
+    paths:
+      - user://images
+      - system://images
+
+  page:
+    type: ReadOnlyStream
+    paths:
+      - user://pages
+
+  account:
+    type: ReadOnlyStream
+    paths:
+      - user://accounts

+ 112 - 0
system/config/system.yaml

@@ -0,0 +1,112 @@
+absolute_urls: false                   # Absolute or relative URLs for `base_url`
+timezone: ''                           # Valid values: http://php.net/manual/en/timezones.php
+default_locale:                        # Default locale (defaults to system)
+param_sep: ':'                         # Parameter separator, use ';' for Apache on windows
+wrapped_site: false                    # For themes/plugins to know if Grav is wrapped by another platform
+
+languages:
+  supported: []                        # List of languages supported. eg: [en, fr, de]
+  include_default_lang: true           # Include the default lang prefix in all URLs
+  translations: true                   # Enable translations by default
+  translations_fallback: true          # Fallback through supported translations if active lang doesn't exist
+  session_store_active: false          # Store active language in session
+  http_accept_language: false          # Attempt to set the language based on http_accept_language header in the browser
+  override_locale: false               # Override the default or system locale with language specific one
+
+home:
+  alias: '/home'                       # Default path for home, ie /
+
+pages:
+  theme: antimatter                    # Default theme (defaults to "antimatter" theme)
+  order:
+    by: default                        # Order pages by "default", "alpha" or "date"
+    dir: asc                           # Default ordering direction, "asc" or "desc"
+  list:
+    count: 20                          # Default item count per page
+  dateformat:
+    default:                           # The default date format Grav expects in the `date: ` field
+    short: 'jS M Y'                    # Short date format
+    long: 'F jS \a\t g:ia'             # Long date format
+  publish_dates: true                  # automatically publish/unpublish based on dates
+  process:
+    markdown: true                     # Process Markdown
+    twig: false                        # Process Twig
+  events:
+    page: true                         # Enable page level events
+    twig: true                         # Enable twig level events
+  markdown:
+    extra: false                       # Enable support for Markdown Extra support (GFM by default)
+    auto_line_breaks: false            # Enable automatic line breaks
+    auto_url_links: false              # Enable automatic HTML links
+    escape_markup: false               # Escape markup tags into entities
+    special_chars:                     # List of special characters to automatically convert to entities
+      '>': 'gt'
+      '<': 'lt'
+  types: [txt,xml,html,json,rss,atom]  # list of valid page types
+  expires: 604800                      # Page expires time in seconds (604800 seconds = 7 days)
+  last_modified: false                 # Set the last modified date header based on file modifcation timestamp
+  etag: false                          # Set the etag header tag
+  vary_accept_encoding: false          # Add `Vary: Accept-Encoding` header
+  redirect_default_route: false        # Automatically redirect to a page's default route
+  redirect_default_code: 301           # Default code to use for redirects
+  redirect_trailing_slash: true        # Handle automatically or 301 redirect a trailing / URL
+  ignore_files: [.DS_Store]            # Files to ignore in Pages
+  ignore_folders: [.git, .idea]        # Folders to ignore in Pages
+  ignore_hidden: true                  # Ignore all Hidden files and folders
+  url_taxonomy_filters: true           # Enable auto-magic URL-based taxonomy filters for page collections
+  fallback_types: [png,jpg,jpeg,gif]   # Allowed types of files found if accessed via Page route
+
+cache:
+  enabled: true                        # Set to true to enable caching
+  check:
+    method: file                       # Method to check for updates in pages: file|folder|none
+  driver: auto                         # One of: auto|file|apc|xcache|memcache|wincache
+  prefix: 'g'                          # Cache prefix string (prevents cache conflicts)
+  lifetime: 604800                     # Lifetime of cached data in seconds (0 = infinite)
+  gzip: false                          # GZip compress the page output
+
+twig:
+  cache: true                          # Set to true to enable twig caching
+  debug: false                         # Enable Twig debug
+  auto_reload: true                    # Refresh cache on changes
+  autoescape: false                    # Autoescape Twig vars
+  undefined_functions: true            # Allow undefined functions
+  undefined_filters: true              # Allow undefined filters
+
+assets:                                # Configuration for Assets Manager (JS, CSS)
+  css_pipeline: false                  # The CSS pipeline is the unification of multiple CSS resources into one file
+  css_minify: true                     # Minify the CSS during pipelining
+  css_minify_windows: false            # Minify Override for Windows platforms. False by default due to ThreadStackSize
+  css_rewrite: true                    # Rewrite any CSS relative URLs during pipelining
+  js_pipeline: false                   # The JS pipeline is the unification of multiple JS resources into one file
+  js_minify: true                      # Minify the JS during pipelining
+  enable_asset_timestamp: false        # Enable asset timestamps
+  collections:
+    jquery: system://assets/jquery/jquery-2.1.4.min.js
+
+errors:
+  display: false                       # Display full backtrace-style error page
+  log: true                            # Log errors to /logs folder
+
+debugger:
+  enabled: false                       # Enable Grav debugger and following settings
+  shutdown:
+    close_connection: true             # Close the connection before calling onShutdown(). false for debugging
+
+images:
+  default_image_quality: 85            # Default image quality to use when resampling images (85%)
+  cache_all: false                     # Cache all image by default
+  debug: false                         # Show an overlay over images indicating the pixel depth of the image when working with retina for example
+
+media:
+  enable_media_timestamp: false        # Enable media timetsamps
+  upload_limit: 0                      # Set maximum upload size in bytes (0 is unlimited)
+  unsupported_inline_types: []         # Array of unsupported media file types to try to display inline
+
+session:
+  enabled: true                        # Enable Session support
+  timeout: 1800                        # Timeout in seconds
+  name: grav-site                      # Name prefix of the session cookie
+
+security:
+  default_hash: $2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK

+ 42 - 0
system/defines.php

@@ -0,0 +1,42 @@
+<?php
+
+// Some standard defines
+define('GRAV', true);
+define('GRAV_VERSION', '1.0.0-rc.4');
+define('DS', '/');
+
+// Directories and Paths
+if (!defined('GRAV_ROOT')) {
+    define('GRAV_ROOT', str_replace(DIRECTORY_SEPARATOR, DS, getcwd()));
+}
+define('ROOT_DIR', GRAV_ROOT . '/');
+define('USER_PATH', 'user/');
+define('USER_DIR', ROOT_DIR . USER_PATH);
+define('SYSTEM_DIR', ROOT_DIR .'system/');
+define('CACHE_DIR', ROOT_DIR . 'cache/');
+define('LOG_DIR', ROOT_DIR .'logs/');
+
+// DEPRECATED: Do not use!
+define('ASSETS_DIR', ROOT_DIR . 'assets/');
+define('IMAGES_DIR', ROOT_DIR . 'images/');
+define('ACCOUNTS_DIR', USER_DIR .'accounts/');
+define('PAGES_DIR', USER_DIR .'pages/');
+define('DATA_DIR', USER_DIR .'data/');
+define('LIB_DIR', SYSTEM_DIR .'src/');
+define('PLUGINS_DIR', USER_DIR .'plugins/');
+define('THEMES_DIR', USER_DIR .'themes/');
+define('VENDOR_DIR', ROOT_DIR .'vendor/');
+// END DEPRECATED
+
+// Some extensions
+define('CONTENT_EXT', '.md');
+define('TEMPLATE_EXT', '.html.twig');
+define('TWIG_EXT', '.twig');
+define('PLUGIN_EXT', '.php');
+define('YAML_EXT', '.yaml');
+
+// Content types
+define('RAW_CONTENT', 1);
+define('TWIG_CONTENT', 2);
+define('TWIG_CONTENT_LIST', 3);
+define('TWIG_TEMPLATES', 4);

+ 37 - 0
system/languages/cs.yaml

@@ -0,0 +1,37 @@
+NICETIME:
+    NO_DATE_PROVIDED: Datum nebylo vloženo
+    BAD_DATE: Chybné datum
+    AGO: zpět
+    FROM_NOW: od teď
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: hodina
+    DAY: den
+    WEEK: týden
+    MONTH: měsíc
+    YEAR: rok
+    DECADE: dekáda
+    SEC: sek
+    MIN: min
+    HR: hod
+    DAY: den
+    WK: t
+    MO: m
+    YR: r
+    DEC: dek
+    SECOND_PLURAL: sekundy
+    MINUTE_PLURAL: minuty
+    HOUR_PLURAL: hodiny
+    DAY_PLURAL: dny
+    WEEK_PLURAL: týdny
+    MONTH_PLURAL: měsíce
+    YEAR_PLURAL: roky
+    DECADE_PLURAL: dekády
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: hod
+    DAY_PLURAL: dny
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: r
+    DEC_PLURAL: dek

+ 43 - 0
system/languages/de.yaml

@@ -0,0 +1,43 @@
+INFLECTOR_IRREGULAR:
+    'person': 'Personen'
+    'man': 'Menschen'
+    'child': 'Kinder'
+    'sex': 'Geschlecht'
+    'move': 'Züge'
+NICETIME:
+    NO_DATE_PROVIDED: Keine Daten vorhanden
+    BAD_DATE: Falsches Datum
+    AGO: her
+    FROM_NOW: ab jetzt
+    SECOND: Sekunde
+    MINUTE: Minute
+    HOUR: Stunde
+    DAY: Tag
+    WEEK: Woche
+    MONTH: Monat
+    YEAR: Jahr
+    DECADE: Dekade
+    SEC: sek
+    MIN: min
+    HR: std
+    DAY: Tag
+    WK: wo
+    MO: mo
+    YR: yh
+    DEC: dec
+    SECOND_PLURAL: Sekunden
+    MINUTE_PLURAL: Minuten
+    HOUR_PLURAL: Stunden
+    DAY_PLURAL: Tage
+    WEEK_PLURAL: Wochen
+    MONTH_PLURAL: Monate
+    YEAR_PLURAL: Jahre
+    DECADE_PLURAL: Dekaden
+    SEC_PLURAL: Sekunden
+    MIN_PLURAL: Minuten
+    HR_PLURAL: Stunden
+    DAY_PLURAL: Tage
+    WK_PLURAL: Wochen
+    MO_PLURAL: Monate
+    YR_PLURAL: Jahre
+    DEC_PLURAL: Dekaden

+ 94 - 0
system/languages/en.yaml

@@ -0,0 +1,94 @@
+FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+    '/s$/i': ''
+INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
+INFLECTOR_IRREGULAR:
+    'person': 'people'
+    'man': 'men'
+    'child': 'children'
+    'sex': 'sexes'
+    'move': 'moves'
+INFLECTOR_ORDINALS:
+    'default': 'th'
+    'first': 'st'
+    'second': 'nd'
+    'third': 'rd'
+NICETIME:
+    NO_DATE_PROVIDED: No date provided
+    BAD_DATE: Bad date
+    AGO: ago
+    FROM_NOW: from now
+    SECOND: second
+    MINUTE: minute
+    HOUR: hour
+    DAY: day
+    WEEK: week
+    MONTH: month
+    YEAR: year
+    DECADE: decade
+    SEC: sec
+    MIN: min
+    HR: hr
+    DAY: day
+    WK: wk
+    MO: mo
+    YR: yr
+    DEC: dec
+    SECOND_PLURAL: seconds
+    MINUTE_PLURAL: minutes
+    HOUR_PLURAL: hours
+    DAY_PLURAL: days
+    WEEK_PLURAL: weeks
+    MONTH_PLURAL: months
+    YEAR_PLURAL: years
+    DECADE_PLURAL: decades
+    SEC_PLURAL: secs
+    MIN_PLURAL: mins
+    HR_PLURAL: hrs
+    DAY_PLURAL: days
+    WK_PLURAL: wks
+    MO_PLURAL: mos
+    YR_PLURAL: yrs
+    DEC_PLURAL: decs

+ 60 - 0
system/languages/fr.yaml

@@ -0,0 +1,60 @@
+INFLECTOR_PLURALS:
+    '/$/': 's'
+    '/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)$/': '\1x'
+    '/(bleu|émeu|landau|lieu|pneu|sarrau)$/': '\1s'
+    '/(b|cor|ém|gemm|soupir|trav|vant|vitr)ail$/': '\1aux'
+    '/(s|x|z)$/': '\1'
+    '/ail$/': 'ails'
+    '/al$/': 'aux'
+    '/s$/i': 's'
+INFLECTOR_SINGULAR:
+    '/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)x$/': '\1'
+    '/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/': '\1ail'
+    '/(journ|chev)aux$/': '\1al'
+    '/ails$/': 'ail'
+    '/s$/i': ''
+INFLECTOR_IRREGULAR:
+    'madame': 'mesdames'
+    'mademoiselle': 'mesdemoiselles'
+    'monsieur': 'messieurs'
+INFLECTOR_ORDINALS:
+    'default': 'ème'
+    'first': 'er'
+    'second': 'nd'
+NICETIME:
+    NO_DATE_PROVIDED: Aucune date
+    BAD_DATE: Date erronée
+    AGO: plus tôt
+    FROM_NOW: à partir de maintenant
+    SECOND: seconde
+    MINUTE: minute
+    HOUR: heure
+    DAY: jour
+    WEEK: semaine
+    MONTH: mois
+    YEAR: an
+    DECADE: décennie
+    SEC: s
+    MIN: m
+    HR: h
+    DAY: j
+    WK: s
+    MO: m
+    YR: a
+    DEC: d
+    SECOND_PLURAL: secondes
+    MINUTE_PLURAL: minutes
+    HOUR_PLURAL: heures
+    DAY_PLURAL: jours
+    WEEK_PLURAL: semaines
+    MONTH_PLURAL: mois
+    YEAR_PLURAL: ans
+    DECADE_PLURAL: décennies
+    SEC_PLURAL: s
+    MIN_PLURAL: m
+    HR_PLURAL: h
+    DAY_PLURAL: j
+    WK_PLURAL: s
+    MO_PLURAL: m
+    YR_PLURAL: a
+    DEC_PLURAL: d

+ 21 - 0
system/languages/it.yaml

@@ -0,0 +1,21 @@
+NICETIME:
+    NO_DATE_PROVIDED: Nessuna data fornita
+    BAD_DATE: Data errata
+    AGO: fa
+    FROM_NOW: da adesso
+    SECOND: secondo
+    MINUTE: minuto
+    HOUR: ora
+    DAY: giorno
+    WEEK: settimana
+    MONTH: mese
+    YEAR: anno
+    DECADE: decade
+    SECOND_PLURAL: secondi
+    MINUTE_PLURAL: minuti
+    HOUR_PLURAL: ore
+    DAY_PLURAL: giorni
+    WEEK_PLURAL: settimane
+    MONTH_PLURAL: mesi
+    YEAR_PLURAL: anni
+    DECADE_PLURAL: decadi

+ 43 - 0
system/languages/nl.yaml

@@ -0,0 +1,43 @@
+INFLECTOR_IRREGULAR:
+    'person': 'personen'
+    'man': 'mensen'
+    'child': 'kinderen'
+    'sex': 'geslacht'
+    'move': 'verplaatsen'
+NICETIME:
+    NO_DATE_PROVIDED: geen datum opgegeven
+    BAD_DATE: Datumformaat onjuist
+    AGO: geleden
+    FROM_NOW: vanaf nu
+    SECOND: seconde
+    MINUTE: minuut
+    HOUR: uur
+    DAY: dag
+    WEEK: week
+    MONTH: maand
+    YEAR: jaar
+    DECADE: decenium
+    SEC: sec
+    MIN: min
+    HR: hr
+    DAY: dag
+    WK: wk
+    MO: ma
+    YR: yr
+    DEC: dec
+    SECOND_PLURAL: seconden
+    MINUTE_PLURAL: minuten
+    HOUR_PLURAL: uren
+    DAY_PLURAL: dagen
+    WEEK_PLURAL: weken
+    MONTH_PLURAL: maanden
+    YEAR_PLURAL: jaren
+    DECADE_PLURAL: decennia
+    SEC_PLURAL: seconden
+    MIN_PLURAL: minuten
+    HR_PLURAL: uren
+    DAY_PLURAL: dagen
+    WK_PLURAL: weken
+    MO_PLURAL: maanden
+    YR_PLURAL: jaren
+    DEC_PLURAL: decs

+ 43 - 0
system/languages/ru.yaml

@@ -0,0 +1,43 @@
+INFLECTOR_IRREGULAR:
+    'person': 'люди'
+    'man': 'человек'
+    'child': 'ребенок'
+    'sex': 'пол'
+    'move': 'движется'
+NICETIME:
+    NO_DATE_PROVIDED: Дата не указана
+    BAD_DATE: Неверная дата
+    AGO: назад
+    FROM_NOW: теперь
+    SECOND: секунда
+    MINUTE: минута
+    HOUR: час
+    DAY: день
+    WEEK: неделя
+    MONTH: месяц
+    YEAR: год
+    DECADE: десятилетие
+    SEC: с
+    MIN: мин
+    HR: ч
+    DAY: д
+    WK: нед
+    MO: мес
+    YR: г.
+    DEC: гг.
+    SECOND_PLURAL: секунды
+    MINUTE_PLURAL: минуты
+    HOUR_PLURAL: часы
+    DAY_PLURAL: дни
+    WEEK_PLURAL: недели
+    MONTH_PLURAL: месяцы
+    YEAR_PLURAL: годы
+    DECADE_PLURAL: десятилетия
+    SEC_PLURAL: с
+    MIN_PLURAL: мин
+    HR_PLURAL: ч
+    DAY_PLURAL: д
+    WK_PLURAL: нед
+    MO_PLURAL: мес
+    YR_PLURAL: г.
+    DEC_PLURAL: гг.

+ 1143 - 0
system/src/Grav/Common/Assets.php

@@ -0,0 +1,1143 @@
+<?php
+namespace Grav\Common;
+
+use Closure;
+use Exception;
+use FilesystemIterator;
+use Grav\Common\Config\Config;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+
+define('CSS_ASSET', true);
+define('JS_ASSET', false);
+
+/**
+ * Handles Asset management (CSS & JS) and also pipelining (combining into a single file for each asset)
+ *
+ * Based on stolz/assets (https://github.com/Stolz/Assets) package modified for use with Grav
+ *
+ * @author  RocketTheme
+ * @license MIT
+ */
+class Assets
+{
+    use GravTrait;
+
+    /** @const Regex to match CSS and JavaScript files */
+    const DEFAULT_REGEX = '/.\.(css|js)$/i';
+
+    /** @const Regex to match CSS files */
+    const CSS_REGEX = '/.\.css$/i';
+
+    /** @const Regex to match JavaScript files */
+    const JS_REGEX = '/.\.js$/i';
+
+    /** @const Regex to match CSS urls */
+    const CSS_URL_REGEX = '{url\([\'\"]?((?!http|//).*?)[\'\"]?\)}';
+
+    /** @const Regex to match CSS sourcemap comments */
+    const CSS_SOURCEMAP_REGEX = '{\/\*# (.*) \*\/}';
+
+    /** @const Regex to match CSS import content */
+    const CSS_IMPORT_REGEX = '{@import(.*);}';
+
+    const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\2>)#is';
+
+
+    /**
+     * Closure used by the pipeline to fetch assets.
+     *
+     * Useful when file_get_contents() function is not available in your PHP
+     * installation or when you want to apply any kind of preprocessing to
+     * your assets before they get pipelined.
+     *
+     * The closure will receive as the only parameter a string with the path/URL of the asset and
+     * it should return the content of the asset file as a string.
+     *
+     * @var Closure
+     */
+    protected $fetch_command;
+
+    // Configuration toggles to enable/disable the pipelining feature
+    protected $css_pipeline = false;
+    protected $js_pipeline = false;
+
+    // The asset holding arrays
+    protected $collections = array();
+    protected $css = array();
+    protected $js = array();
+    protected $inline_css = array();
+    protected $inline_js = array();
+
+    // Some configuration variables
+    protected $config;
+    protected $base_url;
+    protected $timestamp = '';
+    protected $assets_dir;
+    protected $assets_url;
+
+    // Default values for pipeline settings
+    protected $css_minify = true;
+    protected $css_minify_windows = false;
+    protected $css_rewrite = true;
+    protected $js_minify = true;
+
+    // Arrays to hold assets that should NOT be pipelined
+    protected $css_no_pipeline = array();
+    protected $js_no_pipeline = array();
+
+    public function __construct(array $options = array())
+    {
+        // Forward config options
+        if ($options) {
+            $this->config((array)$options);
+        }
+    }
+
+    /**
+     * Set up configuration options.
+     *
+     * All the class properties except 'js' and 'css' are accepted here.
+     * Also, an extra option 'autoload' may be passed containing an array of
+     * assets and/or collections that will be automatically added on startup.
+     *
+     * @param  array $config Configurable options.
+     *
+     * @return $this
+     * @throws \Exception
+     */
+    public function config(array $config)
+    {
+        // Set pipeline modes
+        if (isset($config['css_pipeline'])) {
+            $this->css_pipeline = $config['css_pipeline'];
+        }
+
+        if (isset($config['js_pipeline'])) {
+            $this->js_pipeline = $config['js_pipeline'];
+        }
+
+        // Pipeline requires public dir
+        if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) {
+            throw new \Exception('Assets: Public dir not found');
+        }
+
+        // Set custom pipeline fetch command
+        if (isset($config['fetch_command']) && ($config['fetch_command'] instanceof Closure)) {
+            $this->fetch_command = $config['fetch_command'];
+        }
+
+        // Set CSS Minify state
+        if (isset($config['css_minify'])) {
+            $this->css_minify = $config['css_minify'];
+        }
+
+        if (isset($config['css_minify_windows'])) {
+            $this->css_minify_windows = $config['css_minify_windows'];
+        }
+
+        if (isset($config['css_rewrite'])) {
+            $this->css_rewrite = $config['css_rewrite'];
+        }
+
+        // Set JS Minify state
+        if (isset($config['js_minify'])) {
+            $this->js_minify = $config['js_minify'];
+        }
+
+        // Set collections
+        if (isset($config['collections']) && is_array($config['collections'])) {
+            $this->collections = $config['collections'];
+        }
+
+        // Autoload assets
+        if (isset($config['autoload']) && is_array($config['autoload'])) {
+            foreach ($config['autoload'] as $asset) {
+                $this->add($asset);
+            }
+        }
+
+        // Set timestamp
+        if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
+            $this->timestamp = '?' . self::getGrav()['cache']->getKey();
+        }
+
+
+        return $this;
+    }
+
+    /**
+     * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
+     */
+    public function init()
+    {
+        /** @var Config $config */
+        $config = self::getGrav()['config'];
+        $base_url = self::getGrav()['base_url'];
+        $asset_config = (array)$config->get('system.assets');
+
+        /** @var Locator $locator */
+        $locator = self::$grav['locator'];
+        $this->assets_dir = self::getGrav()['locator']->findResource('asset://') . DS;
+        $this->assets_url = self::getGrav()['locator']->findResource('asset://', false);
+
+        $this->config($asset_config);
+        $this->base_url = $base_url . '/';
+
+        // Register any preconfigured collections
+        foreach ($config->get('system.assets.collections') as $name => $collection) {
+            $this->registerCollection($name, (array)$collection);
+        }
+    }
+
+    /**
+     * Add an asset or a collection of assets.
+     *
+     * It automatically detects the asset type (JavaScript, CSS or collection).
+     * You may add more than one asset passing an array as argument.
+     *
+     * @param  mixed $asset
+     * @param  int   $priority the priority, bigger comes first
+     * @param  bool  $pipeline false if this should not be pipelined
+     *
+     * @return $this
+     */
+    public function add($asset, $priority = null, $pipeline = null)
+    {
+        // More than one asset
+        if (is_array($asset)) {
+            foreach ($asset as $a) {
+                $this->add($a, $priority, $pipeline);
+            }
+        } elseif (isset($this->collections[$asset])) {
+            $this->add($this->collections[$asset], $priority, $pipeline);
+        } else {
+            // Get extension
+            $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
+
+            // JavaScript or CSS
+            if (strlen($extension) > 0) {
+                $extension = strtolower($extension);
+                if ($extension === 'css') {
+                    $this->addCss($asset, $priority, $pipeline);
+                } elseif ($extension === 'js') {
+                    $this->addJs($asset, $priority, $pipeline);
+                }
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a CSS asset.
+     *
+     * It checks for duplicates.
+     * You may add more than one asset passing an array as argument.
+     *
+     * @param  mixed $asset
+     * @param  int $priority the priority, bigger comes first
+     * @param  bool $pipeline false if this should not be pipelined
+     * @param null $group
+     *
+     * @return $this
+     */
+    public function addCss($asset, $priority = null, $pipeline = null, $group = null)
+    {
+        if (is_array($asset)) {
+            foreach ($asset as $a) {
+                $this->addCss($a, $priority, $pipeline, $group);
+            }
+            return $this;
+        } elseif (isset($this->collections[$asset])) {
+            $this->add($this->collections[$asset], $priority, $pipeline, $group);
+            return $this;
+        }
+
+        if (!$this->isRemoteLink($asset)) {
+            $asset = $this->buildLocalLink($asset);
+        }
+
+        $data = [
+            'asset'    => $asset,
+            'priority' => intval($priority ?: 10),
+            'order'    => count($this->css),
+            'pipeline' => $pipeline ?: true,
+            'group' => $group ?: 'head'
+        ];
+
+        // check for dynamic array and merge with defaults
+        $count_args = func_num_args();
+        if (func_num_args() == 2) {
+            $dynamic_arg = func_get_arg(1);
+            if (is_array($dynamic_arg)) {
+                $data = array_merge($data, $dynamic_arg);
+            }
+        }
+
+        $key = md5($asset);
+        if ($asset) {
+            $this->css[$key] = $data;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a JavaScript asset.
+     *
+     * It checks for duplicates.
+     * You may add more than one asset passing an array as argument.
+     *
+     * @param  mixed $asset
+     * @param  int $priority the priority, bigger comes first
+     * @param  bool $pipeline false if this should not be pipelined
+     * @param  string $loading how the asset is loaded (async/defer)
+     * @param  string $group name of the group
+     * @return $this
+     */
+    public function addJs($asset, $priority = null, $pipeline = null, $loading = null, $group = null)
+    {
+        if (is_array($asset)) {
+            foreach ($asset as $a) {
+                $this->addJs($a, $priority, $pipeline, $loading, $group);
+            }
+            return $this;
+        } elseif (isset($this->collections[$asset])) {
+            $this->add($this->collections[$asset], $priority, $pipeline, $loading, $group);
+            return $this;
+        }
+
+        if (!$this->isRemoteLink($asset)) {
+            $asset = $this->buildLocalLink($asset);
+        }
+
+        $data = [
+            'asset'    => $asset,
+            'priority' => intval($priority ?: 10),
+            'order'    => count($this->js),
+            'pipeline' => $pipeline ?: true,
+            'loading'  => $loading ?: '',
+            'group' => $group ?: 'head'
+        ];
+
+        // check for dynamic array and merge with defaults
+        $count_args = func_num_args();
+        if (func_num_args() == 2) {
+            $dynamic_arg = func_get_arg(1);
+            if (is_array($dynamic_arg)) {
+               $data = array_merge($data, $dynamic_arg);
+            }
+        }
+
+        $key = md5($asset);
+        if ($asset) {
+            $this->js[$key] = $data;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Convenience wrapper for async loading of JavaScript
+     *
+     * @param      $asset
+     * @param int  $priority
+     * @param bool $pipeline
+     * @param string $group name of the group
+     *
+     * @deprecated Please use dynamic method with ['loading' => 'async']
+     *
+     * @return \Grav\Common\Assets
+     */
+    public function addAsyncJs($asset, $priority = null, $pipeline = null, $group = null)
+    {
+        return $this->addJs($asset, $priority, $pipeline, 'async', $group);
+    }
+
+    /**
+     * Convenience wrapper for deferred loading of JavaScript
+     *
+     * @param      $asset
+     * @param int  $priority
+     * @param bool $pipeline
+     * @param string $group name of the group
+     *
+     * @deprecated Please use dynamic method with ['loading' => 'defer']
+     *
+     * @return \Grav\Common\Assets
+     */
+    public function addDeferJs($asset, $priority = null, $pipeline = null, $group = null)
+    {
+        return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
+    }
+
+    /**
+     * Add an inline CSS asset.
+     *
+     * It checks for duplicates.
+     * For adding chunks of string-based inline CSS
+     *
+     * @param  mixed $asset
+     * @param  int $priority the priority, bigger comes first
+     * @param null $group
+     *
+     * @return $this
+     */
+    public function addInlineCss($asset, $priority = null, $group = null)
+    {
+        $asset = trim($asset);
+
+        if (is_a($asset, 'Twig_Markup')) {
+            preg_match(self::HTML_TAG_REGEX, $asset, $matches );
+            if (isset($matches[3]))  {
+                $asset = $matches[3];
+            }
+        }
+
+        $data = [
+            'priority'  => intval($priority ?: 10),
+            'order'     => count($this->inline_css),
+            'asset'     => $asset,
+            'group'     => $group ?: 'head'
+        ];
+
+        // check for dynamic array and merge with defaults
+        $count_args = func_num_args();
+        if (func_num_args() == 2) {
+            $dynamic_arg = func_get_arg(1);
+            if (is_array($dynamic_arg)) {
+                $data = array_merge($data, $dynamic_arg);
+            }
+        }
+
+        $key = md5($asset);
+        if (is_string($asset) && !array_key_exists($key, $this->inline_css)) {
+            $this->inline_css[$key] = $data;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add an inline JS asset.
+     *
+     * It checks for duplicates.
+     * For adding chunks of string-based inline JS
+     *
+     * @param  mixed $asset
+     * @param  int   $priority the priority, bigger comes first
+     * @param string $group name of the group
+     *
+     * @return $this
+     */
+    public function addInlineJs($asset, $priority = null, $group = null)
+    {
+        $asset = trim($asset);
+
+        if (is_a($asset, 'Twig_Markup')) {
+            preg_match(self::HTML_TAG_REGEX, $asset, $matches );
+            if (isset($matches[3]))  {
+                $asset = $matches[3];
+            }
+        }
+
+        $data = [
+            'asset'    => $asset,
+            'priority' => intval($priority ?: 10),
+            'order'    => count($this->js),
+            'group' => $group ?: 'head'
+        ];
+
+        // check for dynamic array and merge with defaults
+        $count_args = func_num_args();
+        if (func_num_args() == 2) {
+            $dynamic_arg = func_get_arg(1);
+            if (is_array($dynamic_arg)) {
+                $data = array_merge($data, $dynamic_arg);
+            }
+        }
+
+        $key = md5($asset);
+        if (is_string($asset) && !array_key_exists($key, $this->inline_js)) {
+            $this->inline_js[$key] = $data;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Build the CSS link tags.
+     *
+     * @param  string $group name of the group
+     * @param  array $attributes
+     *
+     * @return string
+     */
+    public function css($group = 'head', $attributes = [])
+    {
+        if (!$this->css) {
+            return null;
+        }
+
+        // Sort array by priorities (larger priority first)
+        if (self::getGrav()) {
+            usort($this->css, function ($a, $b) {
+                if ($a['priority'] == $b['priority']) {
+                    return $b['order'] - $a['order'];
+                }
+                return $a['priority'] - $b['priority'];
+            });
+
+            usort($this->inline_css, function ($a, $b) {
+                if ($a['priority'] == $b['priority']) {
+                    return $b['order'] - $a['order'];
+                }
+                return $a['priority'] - $b['priority'];
+            });
+        }
+        $this->css = array_reverse($this->css);
+        $this->inline_css = array_reverse($this->inline_css);
+
+        $attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
+
+        $output = '';
+        $inline_css = '';
+
+        if ($this->css_pipeline) {
+            $pipeline_result = $this->pipelineCss($group);
+            if ($pipeline_result) {
+                $output .= '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\n";
+            }
+            foreach ($this->css_no_pipeline as $file) {
+                if ($group && $file['group'] == $group) {
+                    $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
+                    $output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
+                }
+            }
+        } else {
+            foreach ($this->css as $file) {
+                if ($group && $file['group'] == $group) {
+                    $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
+                    $output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
+                }
+            }
+        }
+
+        // Render Inline CSS
+        foreach ($this->inline_css as $inline) {
+            if ($group && $inline['group'] == $group) {
+                $inline_css .= $inline['asset'] . "\n";
+            }
+        }
+
+        if ($inline_css) {
+            $output .= "\n<style>\n" . $inline_css . "\n</style>\n";
+        }
+
+
+        return $output;
+    }
+
+    /**
+     * Build the JavaScript script tags.
+     *
+     * @param  string $group name of the group
+     * @param  array $attributes
+     *
+     * @return string
+     */
+    public function js($group = 'head', $attributes = [])
+    {
+        if (!$this->js) {
+            return null;
+        }
+
+        // Sort array by priorities (larger priority first)
+        usort($this->js, function ($a, $b) {
+            if ($a['priority'] == $b['priority']) {
+                return $b['order'] - $a['order'];
+            }
+            return $a['priority'] - $b['priority'];
+        });
+
+        usort($this->inline_js, function ($a, $b) {
+            if ($a['priority'] == $b['priority']) {
+                return $b['order'] - $a['order'];
+            }
+            return $a['priority'] - $b['priority'];
+        });
+
+        $this->js = array_reverse($this->js);
+        $this->inline_js = array_reverse($this->inline_js);
+
+        $attributes = $this->attributes(array_merge(['type' => 'text/javascript'], $attributes));
+
+        $output = '';
+        $inline_js = '';
+
+        if ($this->js_pipeline) {
+            $pipeline_result = $this->pipelineJs($group);
+            if ($pipeline_result) {
+                $output .= '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\n";
+            }
+            foreach ($this->js_no_pipeline as $file) {
+                if ($group && $file['group'] == $group) {
+                    $output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading']. '></script>' . "\n";
+                }
+            }
+        } else {
+            foreach ($this->js as $file) {
+                if ($group && $file['group'] == $group) {
+                    $output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
+                }
+            }
+        }
+
+        // Render Inline JS
+        foreach ($this->inline_js as $inline) {
+            if ($group && $inline['group'] == $group) {
+                $inline_js .= $inline['asset'] . "\n";
+            }
+        }
+
+        if ($inline_js) {
+            $output .= "\n<script>\n" . $inline_js . "\n</script>\n";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Minify and concatenate CSS.
+     *
+     * @return string
+     */
+    protected function pipelineCss($group = 'head')
+    {
+        /** @var Cache $cache */
+        $cache = self::getGrav()['cache'];
+        $key = '?' . $cache->getKey();
+
+        // temporary list of assets to pipeline
+        $temp_css = [];
+
+        // clear no-pipeline assets lists
+        $this->css_no_pipeline = [];
+
+        $file = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group) . '.css';
+
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+        $absolute_path = $this->assets_dir . $file;
+
+        // If pipeline exist return it
+        if (file_exists($absolute_path)) {
+            return $relative_path . $key;
+        }
+
+        // Remove any non-pipeline files
+        foreach ($this->css as $id => $asset) {
+            if ($asset['group'] == $group) {
+                if (!$asset['pipeline']) {
+                    $this->css_no_pipeline[$id] = $asset;
+                } else {
+                    $temp_css[$id] = $asset;
+                }
+            }
+        }
+
+        //if nothing found get out of here!
+        if (count($temp_css) == 0) {
+            return false;
+        }
+
+        $css_minify = $this->css_minify;
+
+        // If this is a Windows server, and minify_windows is false (default value) skip the
+        // minification process because it will cause Apache to die/crash due to insufficient
+        // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
+        if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) {
+            $css_minify = false;
+        }
+
+        // Concatenate files
+        $buffer = $this->gatherLinks($temp_css, CSS_ASSET);
+        if ($css_minify) {
+            $min = new \CSSmin();
+            $buffer = $min->run($buffer);
+        }
+
+        // Write file
+        if (strlen(trim($buffer)) > 0) {
+            file_put_contents($absolute_path, $buffer);
+            return $relative_path . $key;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Minify and concatenate JS files.
+     *
+     * @return string
+     */
+    protected function pipelineJs($group = 'head')
+    {
+        /** @var Cache $cache */
+        $cache = self::getGrav()['cache'];
+        $key = '?' . $cache->getKey();
+
+        // temporary list of assets to pipeline
+        $temp_js = [];
+
+        // clear no-pipeline assets lists
+        $this->js_no_pipeline = [];
+
+        $file = md5(json_encode($this->js) . $this->js_minify . $group) . '.js';
+
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+        $absolute_path = $this->assets_dir . $file;
+
+        // If pipeline exist return it
+        if (file_exists($absolute_path)) {
+            return $relative_path . $key;
+        }
+
+        // Remove any non-pipeline files
+        foreach ($this->js as $id => $asset) {
+            if ($asset['group'] == $group) {
+                if (!$asset['pipeline']) {
+                    $this->js_no_pipeline[] = $asset;
+                } else {
+                    $temp_js[$id] = $asset;
+                }
+            }
+        }
+
+        //if nothing found get out of here!
+        if (count($temp_js) == 0) {
+            return false;
+        }
+
+        // Concatenate files
+        $buffer = $this->gatherLinks($temp_js, JS_ASSET);
+        if ($this->js_minify) {
+            $buffer = \JSMin::minify($buffer);
+        }
+
+        // Write file
+        if (strlen(trim($buffer)) > 0) {
+            file_put_contents($absolute_path, $buffer);
+            return $relative_path . $key;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Return the array of all the registered CSS assets
+     *
+     * @return array
+     */
+    public function getCss()
+    {
+        return $this->css;
+    }
+
+    /**
+     * Return the array of all the registered JS assets
+     *
+     * @return array
+     */
+    public function getJs()
+    {
+        return $this->js;
+    }
+
+    /**
+     * Return the array of all the registered collections
+     *
+     * @return array
+     */
+    public function getCollections()
+    {
+        return $this->collections;
+    }
+
+    /**
+     * Determines if an asset exists as a collection, CSS or JS reference
+     *
+     * @param $asset
+     *
+     * @return bool
+     */
+    public function exists($asset)
+    {
+        if (isset($this->collections[$asset]) ||
+            isset($this->css[$asset]) ||
+            isset($this->js[$asset])) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Add/replace collection.
+     *
+     * @param  string $collectionName
+     * @param  array  $assets
+     * @param bool    $overwrite
+     *
+     * @return $this
+     */
+    public function registerCollection($collectionName, Array $assets, $overwrite = false)
+    {
+        if ($overwrite || !isset($this->collections[$collectionName])) {
+            $this->collections[$collectionName] = $assets;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Reset all assets.
+     *
+     * @return $this
+     */
+    public function reset()
+    {
+        return $this->resetCss()->resetJs();
+    }
+
+    /**
+     * Reset JavaScript assets.
+     *
+     * @return $this
+     */
+    public function resetJs()
+    {
+        $this->js = array();
+
+        return $this;
+    }
+
+    /**
+     * Reset CSS assets.
+     *
+     * @return $this
+     */
+    public function resetCss()
+    {
+        $this->css = array();
+
+        return $this;
+    }
+
+    /**
+     * Add all CSS assets within $directory (relative to public dir).
+     *
+     * @param  string $directory Relative to $this->public_dir
+     *
+     * @return $this
+     */
+    public function addDirCss($directory)
+    {
+        return $this->addDir($directory, self::CSS_REGEX);
+    }
+
+    /**
+     * Add all assets matching $pattern within $directory.
+     *
+     * @param  string $directory Relative to $this->public_dir
+     * @param  string $pattern   (regex)
+     *
+     * @return $this
+     * @throws Exception
+     */
+    public function addDir($directory, $pattern = self::DEFAULT_REGEX)
+    {
+        // Check if public_dir exists
+        if (!is_dir($this->assets_dir)) {
+            throw new Exception('Assets: Public dir not found');
+        }
+
+        // Get files
+        $files = $this->rglob($this->assets_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $this->assets_dir);
+
+        // No luck? Nothing to do
+        if (!$files) {
+            return $this;
+        }
+
+        // Add CSS files
+        if ($pattern === self::CSS_REGEX) {
+            $this->css = array_unique(array_merge($this->css, $files));
+            return $this;
+        }
+
+        // Add JavaScript files
+        if ($pattern === self::JS_REGEX) {
+            $this->js = array_unique(array_merge($this->js, $files));
+            return $this;
+        }
+
+        // Unknown pattern. We must poll to know the extension :(
+        foreach ($files as $asset) {
+            $info = pathinfo($asset);
+            if (isset($info['extension'])) {
+                $ext = strtolower($info['extension']);
+                if ($ext === 'css' && !in_array($asset, $this->css)) {
+                    $this->css[] = $asset;
+                } elseif ($ext === 'js' && !in_array($asset, $this->js)) {
+                    $this->js[] = $asset;
+                }
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Determine whether a link is local or remote.
+     *
+     * Understands both "http://" and "https://" as well as protocol agnostic links "//"
+     *
+     * @param  string $link
+     *
+     * @return bool
+     */
+    protected function isRemoteLink($link)
+    {
+        return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8)
+            || '//' === substr($link, 0, 2));
+    }
+
+    /**
+     * Build local links including grav asset shortcodes
+     *
+     * @param  string $asset the asset string reference
+     *
+     * @return string        the final link url to the asset
+     */
+    protected function buildLocalLink($asset)
+    {
+        try {
+            $asset = self::getGrav()['locator']->findResource($asset, false);
+        } catch (\Exception $e) {
+        }
+
+        return $asset ? $this->base_url . ltrim($asset, '/') : false;
+    }
+
+    /**
+     * Build an HTML attribute string from an array.
+     *
+     * @param  array $attributes
+     *
+     * @return string
+     */
+    protected function attributes(array $attributes)
+    {
+        $html = '';
+
+        foreach ($attributes as $key => $value) {
+            // For numeric keys we will assume that the key and the value are the same
+            // as this will convert HTML attributes such as "required" to a correct
+            // form like required="required" instead of using incorrect numerics.
+            if (is_numeric($key)) {
+                $key = $value;
+            }
+            if (is_array($value)) {
+                $value = implode(' ', $value);
+            }
+
+            $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
+            $html .= ' ' . $element;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Download and concatenate the content of several links.
+     *
+     * @param  array $links
+     * @param  bool $css
+     *
+     * @return string
+     */
+    protected function gatherLinks(array $links, $css = true)
+    {
+
+
+        $buffer = '';
+        $local = true;
+
+        foreach ($links as $asset) {
+            $link = $asset['asset'];
+            $relative_path = $link;
+
+            if ($this->isRemoteLink($link)) {
+                $local = false;
+                if ('//' === substr($link, 0, 2)) {
+                    $link = 'http:' . $link;
+                }
+            } else {
+                // Fix to remove relative dir if grav is in one
+                if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
+                    $base_url = '#' . preg_quote($this->base_url, '#') . '#';
+                    $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
+                }
+
+                $relative_dir = dirname($relative_path);
+                $link = ROOT_DIR . $relative_path;
+            }
+
+            $file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
+
+            // No file found, skip it...
+            if ($file === false) {
+                continue;
+            }
+
+            // Double check last character being
+            if (!$css) {
+                $file = rtrim($file, ' ;') . ';';
+            }
+
+            // If this is CSS + the file is local + rewrite enabled
+            if ($css && $local && $this->css_rewrite) {
+                $file = $this->cssRewrite($file, $relative_dir);
+            }
+
+            $buffer .= $file;
+        }
+
+        // Pull out @imports and move to top
+        if ($css) {
+            $buffer = $this->moveImports($buffer);
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * Finds relative CSS urls() and rewrites the URL with an absolute one
+     *
+     * @param $file                 the css source file
+     * @param $relative_path        relative path to the css file
+     *
+     * @return mixed
+     */
+    protected function cssRewrite($file, $relative_path)
+    {
+        // Strip any sourcemap comments
+        $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
+
+        // Find any css url() elements, grab the URLs and calculate an absolute path
+        // Then replace the old url with the new one
+        $file = preg_replace_callback(
+            self::CSS_URL_REGEX,
+            function ($matches) use ($relative_path) {
+
+                $old_url = $matches[1];
+
+                // ensure this is not a data url
+                if (strpos($old_url, 'data:') === 0) {
+                    return $matches[0];
+                }
+
+                $new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
+
+                return str_replace($old_url, $new_url, $matches[0]);
+            },
+            $file
+        );
+
+        return $file;
+    }
+
+    /**
+     * Moves @import statements to the top of the file per the CSS specification
+     *
+     * @param  string $file the file containing the combined CSS files
+     *
+     * @return string       the modified file with any @imports at the top of the file
+     */
+    protected function moveImports($file)
+    {
+        $this->imports = array();
+
+        $file = preg_replace_callback(
+            self::CSS_IMPORT_REGEX,
+            function ($matches) {
+                $this->imports[] = $matches[0];
+                return '';
+            },
+            $file
+        );
+
+        return implode("\n", $this->imports) . "\n\n" . $file;
+    }
+
+    /**
+     * Recursively get files matching $pattern within $directory.
+     *
+     * @param  string $directory
+     * @param  string $pattern (regex)
+     * @param  string $ltrim   Will be trimmed from the left of the file path
+     *
+     * @return array
+     */
+    protected function rglob($directory, $pattern, $ltrim = null)
+    {
+        $iterator = new RegexIterator(
+            new RecursiveIteratorIterator(
+                new RecursiveDirectoryIterator(
+                    $directory,
+                    FilesystemIterator::SKIP_DOTS
+                )
+            ),
+            $pattern
+        );
+        $offset = strlen($ltrim);
+        $files = array();
+
+        foreach ($iterator as $file) {
+            $files[] = substr($file->getPathname(), $offset);
+        }
+
+        return $files;
+    }
+
+    /**
+     * Add all JavaScript assets within $directory.
+     *
+     * @param  string $directory Relative to $this->public_dir
+     *
+     * @return $this
+     */
+    public function addDirJs($directory)
+    {
+        return $this->addDir($directory, self::JS_REGEX);
+    }
+
+    public function __toString()
+    {
+        return '';
+    }
+
+    /**
+     * @param $a
+     * @param $b
+     *
+     * @return mixed
+     */
+    protected function priorityCompare($a, $b)
+    {
+        return $a ['priority'] - $b ['priority'];
+    }
+
+}

+ 130 - 0
system/src/Grav/Common/Backup/ZipBackup.php

@@ -0,0 +1,130 @@
+<?php
+namespace Grav\Common\Backup;
+
+use Grav\Common\GravTrait;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Inflector;
+
+/**
+ * The ZipBackup class lets you create simple zip-backups of a grav site
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class ZipBackup
+{
+    use GravTrait;
+
+    protected static $ignorePaths = [
+        'backup',
+        'cache',
+        'images',
+        'logs'
+    ];
+
+    protected static $ignoreFolders = [
+        '.git',
+        '.svn',
+        '.hg',
+        '.idea'
+    ];
+
+    public static function backup($destination = null, callable $messager = null)
+    {
+        if (!$destination) {
+            $destination = self::getGrav()['locator']->findResource('backup://', true);
+
+            if (!$destination)
+                throw new \RuntimeException('The backup folder is missing.');
+
+            Folder::mkdir($destination);
+        }
+
+        $name = self::getGrav()['config']->get('site.title', basename(GRAV_ROOT));
+
+        $inflector = new Inflector();
+
+        if (is_dir($destination)) {
+            $date = date('YmdHis', time());
+            $filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip';
+            $destination = rtrim($destination, DS) . DS . $filename;
+        }
+
+        $messager && $messager([
+            'type' => 'message',
+            'level' => 'info',
+            'message' => 'Creating new Backup "' . $destination . '"'
+        ]);
+        $messager && $messager([
+            'type' => 'message',
+            'level' => 'info',
+            'message' => ''
+        ]);
+
+        $zip = new \ZipArchive();
+        $zip->open($destination, \ZipArchive::CREATE);
+
+        static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
+
+        $messager && $messager([
+            'type' => 'progress',
+            'percentage' => false,
+            'complete' => true
+        ]);
+
+        $messager && $messager([
+            'type' => 'message',
+            'level' => 'info',
+            'message' => ''
+        ]);
+        $messager && $messager([
+            'type' => 'message',
+            'level' => 'info',
+            'message' => 'Saving and compressing archive...'
+        ]);
+
+        $zip->close();
+
+        return $destination;
+    }
+
+    /**
+     * @param $folder
+     * @param $zipFile
+     * @param $exclusiveLength
+     * @param $messager
+     */
+    private static function folderToZip($folder, \ZipArchive &$zipFile, $exclusiveLength, callable $messager = null)
+    {
+        $handle = opendir($folder);
+        while (false !== $f = readdir($handle)) {
+            if ($f != '.' && $f != '..') {
+                $filePath = "$folder/$f";
+                // Remove prefix from file path before add to zip.
+                $localPath = substr($filePath, $exclusiveLength);
+
+                if (in_array($f, static::$ignoreFolders)) {
+                    continue;
+                } elseif (in_array($localPath, static::$ignorePaths)) {
+                    $zipFile->addEmptyDir($f);
+                    continue;
+                }
+
+                if (is_file($filePath)) {
+                    $zipFile->addFile($filePath, $localPath);
+
+                    $messager && $messager([
+                        'type' => 'progress',
+                        'percentage' => false,
+                        'complete' => false
+                    ]);
+                } elseif (is_dir($filePath)) {
+                    // Add sub-directory.
+                    $zipFile->addEmptyDir($localPath);
+                    static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
+                }
+            }
+        }
+        closedir($handle);
+    }
+}

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

@@ -0,0 +1,58 @@
+<?php
+namespace Grav\Common;
+
+/**
+ * Simple wrapper for the very simple parse_user_agent() function
+ */
+class Browser
+{
+
+    protected $useragent = [];
+
+    public function __construct()
+    {
+        try {
+            $this->useragent = parse_user_agent();
+        } catch (\InvalidArgumentException $e) {
+            $this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
+        }
+    }
+
+    public function getBrowser()
+    {
+        return strtolower($this->useragent['browser']);
+    }
+
+    public function getPlatform()
+    {
+        return strtolower($this->useragent['platform']);
+    }
+
+    public function getLongVersion()
+    {
+        return $this->useragent['version'];
+    }
+
+    public function getVersion()
+    {
+        $version = explode('.', $this->getLongVersion());
+        return intval($version[0]);
+    }
+
+    /**
+     * Determine if the request comes from a human, or from a bot/crawler
+     */
+    public function isHuman()
+    {
+        $browser = $this->getBrowser();
+        if (empty($browser)) {
+            return false;
+        }
+
+        if (preg_match('~(bot|crawl)~i', $browser)) {
+            return false;
+        }
+
+        return true;
+    }
+}

+ 314 - 0
system/src/Grav/Common/Cache.php

@@ -0,0 +1,314 @@
+<?php
+namespace Grav\Common;
+
+use \Doctrine\Common\Cache\Cache as DoctrineCache;
+use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
+
+/**
+ * The GravCache object is used throughout Grav to store and retrieve cached data.
+ * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
+ *
+ * APC
+ * XCache
+ * RedisCache
+ * MemCache
+ * MemCacheD
+ * FileSystem
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Cache extends Getters
+{
+    /**
+     * @var string Cache key.
+     */
+    protected $key;
+
+    protected $lifetime;
+    protected $now;
+
+    protected $config;
+
+    /**
+     * @var DoctrineCache
+     */
+    protected $driver;
+
+    /**
+     * @var bool
+     */
+    protected $enabled;
+
+    protected $cache_dir;
+
+    protected static $standard_remove = [
+        'cache/twig/',
+        'cache/doctrine/',
+        'cache/compiled/',
+        'cache/validated-',
+        'images/',
+        'assets/',
+    ];
+
+    protected static $all_remove = [
+        'cache/',
+        'images/',
+        'assets/'
+    ];
+
+    protected static $assets_remove = [
+        'assets/'
+    ];
+
+    protected static $images_remove = [
+        'images/'
+    ];
+
+    protected static $cache_remove = [
+        'cache/'
+    ];
+
+    /**
+     * Constructor
+     *
+     * @params Grav $grav
+     */
+    public function __construct(Grav $grav)
+    {
+        $this->init($grav);
+    }
+
+    /**
+     * Initialization that sets a base key and the driver based on configuration settings
+     *
+     * @param  Grav $grav
+     * @return void
+     */
+    public function init(Grav $grav)
+    {
+        /** @var Config $config */
+        $this->config = $grav['config'];
+        $this->now = time();
+
+        $this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
+
+        /** @var Uri $uri */
+        $uri = $grav['uri'];
+
+        $prefix = $this->config->get('system.cache.prefix');
+
+        $this->enabled = (bool) $this->config->get('system.cache.enabled');
+
+        // Cache key allows us to invalidate all cache on configuration changes.
+        $this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
+
+        $this->driver = $this->getCacheDriver();
+
+        // Set the cache namespace to our unique key
+        $this->driver->setNamespace($this->key);
+    }
+
+    /**
+     * Automatically picks the cache mechanism to use.  If you pick one manually it will use that
+     * If there is no config option for $driver in the config, or it's set to 'auto', it will
+     * pick the best option based on which cache extensions are installed.
+     *
+     * @return DoctrineCacheDriver  The cache driver to use
+     */
+    public function getCacheDriver()
+    {
+        $setting = $this->config->get('system.cache.driver');
+        $driver_name = 'file';
+
+        if (!$setting || $setting == 'auto') {
+            if (extension_loaded('apc')) {
+                $driver_name = 'apc';
+            } elseif (extension_loaded('wincache')) {
+                $driver_name = 'wincache';
+            } elseif (extension_loaded('xcache')) {
+                $driver_name = 'xcache';
+            }
+        } else {
+            $driver_name = $setting;
+        }
+
+        switch ($driver_name) {
+            case 'apc':
+                $driver = new \Doctrine\Common\Cache\ApcCache();
+                break;
+
+            case 'wincache':
+                $driver = new \Doctrine\Common\Cache\WinCacheCache();
+                break;
+
+            case 'xcache':
+                $driver = new \Doctrine\Common\Cache\XcacheCache();
+                break;
+
+            case 'memcache':
+                $memcache = new \Memcache();
+                $memcache->connect($this->config->get('system.cache.memcache.server','localhost'),
+                                   $this->config->get('system.cache.memcache.port', 11211));
+                $driver = new \Doctrine\Common\Cache\MemcacheCache();
+                $driver->setMemcache($memcache);
+                break;
+
+            case 'redis':
+                $redis = new \Redis();
+                $redis->connect($this->config->get('system.cache.redis.server','localhost'),
+                                $this->config->get('system.cache.redis.port', 6379));
+
+                $driver = new \Doctrine\Common\Cache\RedisCache();
+                $driver->setRedis($redis);
+                break;
+
+            default:
+                $driver = new \Doctrine\Common\Cache\FilesystemCache($this->cache_dir);
+                break;
+        }
+
+        return $driver;
+    }
+
+    /**
+     * Gets a cached entry if it exists based on an id. If it does not exist, it returns false
+     *
+     * @param  string $id the id of the cached entry
+     * @return object     returns the cached entry, can be any type, or false if doesn't exist
+     */
+    public function fetch($id)
+    {
+        if ($this->enabled) {
+            return $this->driver->fetch($id);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Stores a new cached entry.
+     *
+     * @param  string $id       the id of the cached entry
+     * @param  array|object $data     the data for the cached entry to store
+     * @param  int $lifetime    the lifetime to store the entry in seconds
+     */
+    public function save($id, $data, $lifetime = null)
+    {
+        if ($this->enabled) {
+            if ($lifetime === null) {
+                $lifetime = $this->getLifetime();
+            }
+            $this->driver->save($id, $data, $lifetime);
+        }
+    }
+
+    /**
+     * Getter method to get the cache key
+     */
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    /**
+     * Helper method to clear all Grav caches
+     *
+     * @param string $remove    standard|all|assets-only|images-only|cache-only
+     *
+     * @return array
+     */
+    public static function clearCache($remove = 'standard')
+    {
+
+        $output = [];
+        $user_config = USER_DIR . 'config/system.yaml';
+
+        switch($remove) {
+            case 'all':
+                $remove_paths = self::$all_remove;
+                break;
+            case 'assets-only':
+                $remove_paths = self::$assets_remove;
+                break;
+            case 'images-only':
+                $remove_paths = self::$images_remove;
+                break;
+            case 'cache-only':
+                $remove_paths = self::$cache_remove;
+                break;
+            default:
+                $remove_paths = self::$standard_remove;
+        }
+
+
+        foreach ($remove_paths as $path) {
+
+            $anything = false;
+            $files = glob(ROOT_DIR . $path . '*');
+
+            if (is_array($files)) {
+                foreach ($files as $file) {
+                    if (is_file($file)) {
+                        if (@unlink($file)) {
+                            $anything = true;
+                        }
+                    } elseif (is_dir($file)) {
+                        if (@Folder::delete($file)) {
+                            $anything = true;
+                        }
+                    }
+                }
+            }
+
+            if ($anything) {
+                $output[] = '<red>Cleared:  </red>' . $path . '*';
+            }
+        }
+
+        $output[] = '';
+
+        if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
+            touch($user_config);
+
+            $output[] = '<red>Touched: </red>' . $user_config;
+            $output[] = '';
+        }
+
+        return $output;
+    }
+
+
+    /**
+     * Set the cache lifetime programmatically
+     *
+     * @param int $future timestamp
+     */
+    public function setLifetime($future)
+    {
+        if (!$future) {
+            return;
+        }
+
+        $interval = $future - $this->now;
+        if ($interval > 0 && $interval < $this->getLifetime()) {
+            $this->lifetime = $interval;
+        }
+    }
+
+
+    /**
+     * Retrieve the cache lifetime (in seconds)
+     *
+     * @return mixed
+     */
+    public function getLifetime()
+    {
+        if ($this->lifetime === null) {
+            $this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default
+        }
+
+        return $this->lifetime;
+    }
+}

+ 55 - 0
system/src/Grav/Common/Composer.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Grav\Common;
+
+/**
+ * Offers composer helper methods.
+ *
+ * @author  eschmar
+ * @license MIT
+ */
+class Composer
+{
+    /** @const Default composer location */
+    const DEFAULT_PATH = "bin/composer.phar";
+
+    /**
+     * Returns the location of composer.
+     *
+     * @return string
+     */
+    public static function getComposerLocation()
+    {
+        if (!function_exists('shell_exec') || strtolower(substr(PHP_OS, 0, 3)) === 'win') {
+            return self::DEFAULT_PATH;
+        }
+
+        // check for global composer install
+        $path = trim(shell_exec("command -v composer"));
+
+        // fall back to grav bundled composer
+        if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
+            $path = self::DEFAULT_PATH;
+        }
+
+        return $path;
+    }
+
+    public static function getComposerExecutor()
+    {
+        $executor = PHP_BINARY . ' ';
+        $composer = static::getComposerLocation();
+
+        if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
+            $file = fopen($composer, 'r');
+            $firstLine = fgets($file);
+            fclose($file);
+
+            if (!preg_match('/^#!.+php/i', $firstLine)) {
+                $executor = '';
+            }
+        }
+
+        return $executor . $composer;
+    }
+}

+ 207 - 0
system/src/Grav/Common/Config/Blueprints.php

@@ -0,0 +1,207 @@
+<?php
+namespace Grav\Common\Config;
+
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Grav;
+use Grav\Common\Filesystem\Folder;
+use RocketTheme\Toolbox\Blueprints\Blueprints as BaseBlueprints;
+use RocketTheme\Toolbox\File\PhpFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+/**
+ * The Blueprints class contains configuration rules.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Blueprints extends BaseBlueprints
+{
+    protected $grav;
+    protected $files = [];
+    protected $blueprints;
+
+    public function __construct(array $serialized = null, Grav $grav = null)
+    {
+        parent::__construct($serialized);
+        $this->grav = $grav ?: Grav::instance();
+    }
+
+    public function init()
+    {
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+
+        $blueprints = $locator->findResources('blueprints://config');
+        $plugins = $locator->findResources('plugins://');
+
+        $blueprintFiles = $this->getBlueprintFiles($blueprints, $plugins);
+
+        $this->loadCompiledBlueprints($plugins + $blueprints, $blueprintFiles);
+    }
+
+    protected function loadCompiledBlueprints($blueprints, $blueprintFiles)
+    {
+        $checksum = md5(serialize($blueprints));
+        $filename = CACHE_DIR . 'compiled/blueprints/' . $checksum .'.php';
+        $checksum .= ':'.md5(serialize($blueprintFiles));
+        $class = get_class($this);
+        $file = PhpFile::instance($filename);
+
+        if ($file->exists()) {
+            $cache = $file->exists() ? $file->content() : null;
+        } else {
+            $cache = null;
+        }
+
+
+        // Load real file if cache isn't up to date (or is invalid).
+        if (
+            !is_array($cache)
+            || empty($cache['checksum'])
+            || empty($cache['$class'])
+            || $cache['checksum'] != $checksum
+            || $cache['@class'] != $class
+        ) {
+            // Attempt to lock the file for writing.
+            $file->lock(false);
+
+            // Load blueprints.
+            $this->blueprints = new Blueprints();
+            foreach ($blueprintFiles as $key => $files) {
+                $this->loadBlueprints($key);
+            }
+
+            $cache = [
+                '@class' => $class,
+                'checksum' => $checksum,
+                'files' => $blueprintFiles,
+                'data' => $this->blueprints->toArray()
+            ];
+
+            // If compiled file wasn't already locked by another process, save it.
+            if ($file->locked() !== false) {
+                $file->save($cache);
+                $file->unlock();
+            }
+        } else {
+            $this->blueprints = new Blueprints($cache['data']);
+        }
+    }
+
+    /**
+     * Load global blueprints.
+     *
+     * @param string $key
+     * @param array $files
+     */
+    public function loadBlueprints($key, array $files = null)
+    {
+        if (is_null($files)) {
+            $files = $this->files[$key];
+        }
+        foreach ($files as $name => $item) {
+            $file = CompiledYamlFile::instance($item['file']);
+            $this->blueprints->embed($name, $file->content(), '/');
+        }
+    }
+
+    /**
+     * Get all blueprint files (including plugins).
+     *
+     * @param array $blueprints
+     * @param array $plugins
+     * @return array
+     */
+    protected function getBlueprintFiles(array $blueprints, array $plugins)
+    {
+        $list = [];
+        foreach (array_reverse($plugins) as $folder) {
+            $list += $this->detectPlugins($folder, true);
+        }
+        foreach (array_reverse($blueprints) as $folder) {
+            $list += $this->detectConfig($folder, true);
+        }
+        return $list;
+    }
+
+    /**
+     * Detects all plugins with a configuration file and returns last modification time.
+     *
+     * @param  string $lookup Location to look up from.
+     * @param  bool $blueprints
+     * @return array
+     * @internal
+     */
+    protected function detectPlugins($lookup = SYSTEM_DIR, $blueprints = false)
+    {
+        $find = $blueprints ? 'blueprints.yaml' : '.yaml';
+        $location = $blueprints ? 'blueprintFiles' : 'configFiles';
+        $path = trim(Folder::getRelativePath($lookup), '/');
+        if (isset($this->{$location}[$path])) {
+            return [$path => $this->{$location}[$path]];
+        }
+
+        $list = [];
+
+        if (is_dir($lookup)) {
+            $iterator = new \DirectoryIterator($lookup);
+
+            /** @var \DirectoryIterator $directory */
+            foreach ($iterator as $directory) {
+                if (!$directory->isDir() || $directory->isDot()) {
+                    continue;
+                }
+
+                $name = $directory->getBasename();
+                $filename = "{$path}/{$name}/" . ($find && $find[0] != '.' ? $find : $name . $find);
+
+                if (is_file($filename)) {
+                    $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)];
+                }
+            }
+        }
+
+        $this->{$location}[$path] = $list;
+
+        return [$path => $list];
+    }
+
+    /**
+     * Detects all plugins with a configuration file and returns last modification time.
+     *
+     * @param  string $lookup Location to look up from.
+     * @param  bool $blueprints
+     * @return array
+     * @internal
+     */
+    protected function detectConfig($lookup = SYSTEM_DIR, $blueprints = false)
+    {
+        $location = $blueprints ? 'blueprintFiles' : 'configFiles';
+        $path = trim(Folder::getRelativePath($lookup), '/');
+        if (isset($this->{$location}[$path])) {
+            return [$path => $this->{$location}[$path]];
+        }
+
+        if (is_dir($lookup)) {
+            // Find all system and user configuration files.
+            $options = [
+                'compare' => 'Filename',
+                'pattern' => '|\.yaml$|',
+                'filters' => [
+                    'key' => '|\.yaml$|',
+                    'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
+                        return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
+                    }],
+                'key' => 'SubPathname'
+            ];
+
+            $list = Folder::all($lookup, $options);
+        } else {
+            $list = [];
+        }
+
+        $this->{$location}[$path] = $list;
+
+        return [$path => $list];
+    }
+}

+ 479 - 0
system/src/Grav/Common/Config/Config.php

@@ -0,0 +1,479 @@
+<?php
+namespace Grav\Common\Config;
+
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Grav;
+use Grav\Common\Data\Data;
+use RocketTheme\Toolbox\Blueprints\Blueprints;
+use RocketTheme\Toolbox\File\PhpFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+/**
+ * The Config class contains configuration information.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Config extends Data
+{
+    protected $grav;
+    protected $streams = [
+        'system' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['system'],
+            ]
+        ],
+        'user' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user'],
+            ]
+        ],
+        'blueprints' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://blueprints', 'system/blueprints'],
+            ]
+        ],
+        'config' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://config', 'system/config'],
+            ]
+        ],
+        'plugins' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://plugins'],
+             ]
+        ],
+        'plugin' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://plugins'],
+            ]
+        ],
+        'themes' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://themes'],
+            ]
+        ],
+        'languages' => [
+            'type' => 'ReadOnlyStream',
+            'prefixes' => [
+                '' => ['user://languages', 'system/languages'],
+            ]
+        ],
+        'cache' => [
+            'type' => 'Stream',
+            'prefixes' => [
+                '' => ['cache'],
+                'images' => ['images']
+            ]
+        ],
+        'log' => [
+            'type' => 'Stream',
+            'prefixes' => [
+                '' => ['logs']
+            ]
+        ],
+        'backup' => [
+            'type' => 'Stream',
+            'prefixes' => [
+                '' => ['backup']
+            ]
+        ]
+    ];
+
+    protected $setup = [];
+
+    protected $blueprintFiles = [];
+    protected $configFiles = [];
+    protected $languageFiles = [];
+    protected $checksum;
+    protected $timestamp;
+
+    protected $configLookup;
+    protected $blueprintLookup;
+    protected $pluginLookup;
+    protected $languagesLookup;
+
+    protected $finder;
+    protected $environment;
+    protected $messages = [];
+
+    protected $languages;
+
+    public function __construct(array $setup = array(), Grav $grav = null, $environment = null)
+    {
+        $this->grav = $grav ?: Grav::instance();
+        $this->finder = new ConfigFinder;
+        $this->environment = $environment ?: 'localhost';
+        $this->messages[] = 'Environment Name: ' . $this->environment;
+
+        // Make sure that
+        if (!isset($setup['streams']['schemes'])) {
+            $setup['streams']['schemes'] = [];
+        }
+        $setup['streams']['schemes'] += $this->streams;
+
+        $setup = $this->autoDetectEnvironmentConfig($setup);
+
+        $this->setup = $setup;
+        parent::__construct($setup);
+
+        $this->check();
+    }
+
+    public function key()
+    {
+        return $this->checksum();
+    }
+
+    public function reload()
+    {
+        $this->items = $this->setup;
+        $this->check();
+        $this->init();
+        $this->debug();
+
+        return $this;
+    }
+
+    protected function check()
+    {
+        $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null;
+        if (!is_array($streams)) {
+            throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
+        }
+        $diff = array_keys(array_diff_key($this->streams, $streams));
+        if ($diff) {
+            throw new \InvalidArgumentException(
+                sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff))
+            );
+        }
+    }
+
+    public function debug()
+    {
+        foreach ($this->messages as $message) {
+            $this->grav['debugger']->addMessage($message);
+        }
+        $this->messages = [];
+    }
+
+    public function init()
+    {
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+
+        $this->configLookup = $locator->findResources('config://');
+        $this->blueprintLookup = $locator->findResources('blueprints://config');
+        $this->pluginLookup = $locator->findResources('plugins://');
+
+
+        $this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master');
+        $this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master');
+
+        // process languages if supported
+        if ($this->get('system.languages.translations', true)) {
+            $this->languagesLookup = $locator->findResources('languages://');
+            $this->loadCompiledLanguages($this->languagesLookup, $this->pluginLookup, 'master');
+        }
+
+        $this->initializeLocator($locator);
+    }
+
+    public function checksum()
+    {
+        if (empty($this->checksum)) {
+            $checkBlueprints = $this->get('system.cache.check.blueprints', false);
+            $checkLanguages = $this->get('system.cache.check.languages', false);
+            $checkConfig = $this->get('system.cache.check.config', true);
+            $checkSystem = $this->get('system.cache.check.system', true);
+
+            if (!$checkBlueprints && !$checkLanguages && !$checkConfig && !$checkSystem) {
+                $this->messages[] = 'Skip configuration timestamp check.';
+                return false;
+            }
+
+            // Generate checksum according to the configuration settings.
+            if (!$checkConfig) {
+                // Just check changes in system.yaml files and ignore all the other files.
+                $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : [];
+            } else {
+                // Check changes in all configuration files.
+                $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup);
+            }
+
+            if ($checkBlueprints) {
+                $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup);
+            } else {
+                $cb = [];
+            }
+
+            if ($checkLanguages) {
+                $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup);
+            } else {
+                $cl = [];
+            }
+
+            $this->checksum = md5(json_encode([$cc, $cb, $cl]));
+        }
+
+        return $this->checksum;
+    }
+
+    protected function autoDetectEnvironmentConfig($items)
+    {
+        $environment = $this->environment;
+        $env_stream = 'user://'.$environment.'/config';
+
+        if (file_exists(USER_DIR.$environment.'/config')) {
+            array_unshift($items['streams']['schemes']['config']['prefixes'][''], $env_stream);
+        }
+
+        return $items;
+    }
+
+    protected function loadCompiledBlueprints($blueprints, $plugins, $filename = null)
+    {
+        $checksum = md5(json_encode($blueprints));
+        $filename = $filename
+            ? CACHE_DIR . 'compiled/blueprints/' . $filename . '-' . $this->environment . '.php'
+            : CACHE_DIR . 'compiled/blueprints/' . $checksum . '-' . $this->environment . '.php';
+        $file = PhpFile::instance($filename);
+        $cache = $file->exists() ? $file->content() : null;
+        $blueprintFiles = $this->finder->locateBlueprintFiles($blueprints, $plugins);
+        $checksum .= ':'.md5(json_encode($blueprintFiles));
+        $class = get_class($this);
+
+        // Load real file if cache isn't up to date (or is invalid).
+        if (
+            !is_array($cache)
+            || !isset($cache['checksum'])
+            || !isset($cache['@class'])
+            || $cache['checksum'] != $checksum
+            || $cache['@class'] != $class
+        ) {
+            // Attempt to lock the file for writing.
+            $file->lock(false);
+
+            // Load blueprints.
+            $this->blueprints = new Blueprints;
+            foreach ($blueprintFiles as $files) {
+                $this->loadBlueprintFiles($files);
+            }
+
+            $cache = [
+                '@class' => $class,
+                'checksum' => $checksum,
+                'files' => $blueprintFiles,
+                'data' => $this->blueprints->toArray()
+            ];
+            // If compiled file wasn't already locked by another process, save it.
+            if ($file->locked() !== false) {
+                $this->messages[] = 'Saving compiled blueprints.';
+                $file->save($cache);
+                $file->unlock();
+            }
+        } else {
+            $this->blueprints = new Blueprints($cache['data']);
+        }
+    }
+
+    protected function loadCompiledConfig($configs, $plugins, $filename = null)
+    {
+        $filename = $filename
+            ? CACHE_DIR . 'compiled/config/' . $filename . '-' . $this->environment . '.php'
+            : CACHE_DIR . 'compiled/config/' . $checksum . '-' . $this->environment . '.php';
+        $file = PhpFile::instance($filename);
+        $cache = $file->exists() ? $file->content() : null;
+        $class = get_class($this);
+        $checksum = $this->checksum();
+
+        if (
+            !is_array($cache)
+            || !isset($cache['checksum'])
+            || !isset($cache['@class'])
+            || $cache['@class'] != $class
+        ) {
+            $this->messages[] = 'No cached configuration, compiling new configuration..';
+        } else if ($cache['checksum'] !== $checksum) {
+            $this->messages[] = 'Configuration checksum mismatch, reloading configuration..';
+        } else {
+            $this->messages[] = 'Configuration checksum matches, using cached version.';
+
+            $this->items = $cache['data'];
+            return;
+        }
+
+        $configFiles = $this->finder->locateConfigFiles($configs, $plugins);
+
+        // Attempt to lock the file for writing.
+        $file->lock(false);
+
+        // Load configuration.
+        foreach ($configFiles as $files) {
+            $this->loadConfigFiles($files);
+        }
+        $cache = [
+            '@class' => $class,
+            'timestamp' => time(),
+            'checksum' => $checksum,
+            'data' => $this->toArray()
+        ];
+
+        // If compiled file wasn't already locked by another process, save it.
+        if ($file->locked() !== false) {
+            $this->messages[] = 'Saving compiled configuration.';
+            $file->save($cache);
+            $file->unlock();
+        }
+
+        $this->items = $cache['data'];
+    }
+
+    /**
+     * @param      $languages
+     * @param      $plugins
+     * @param null $filename
+     */
+    protected function loadCompiledLanguages($languages, $plugins, $filename = null)
+    {
+        $checksum = md5(json_encode($languages));
+        $filename = $filename
+            ? CACHE_DIR . 'compiled/languages/' . $filename . '-' . $this->environment . '.php'
+            : CACHE_DIR . 'compiled/languages/' . $checksum . '-' . $this->environment . '.php';
+        $file = PhpFile::instance($filename);
+        $cache = $file->exists() ? $file->content() : null;
+        $languageFiles = $this->finder->locateLanguageFiles($languages, $plugins);
+        $checksum .= ':' . md5(json_encode($languageFiles));
+        $class = get_class($this);
+
+        // Load real file if cache isn't up to date (or is invalid).
+        if (
+            !is_array($cache)
+            || !isset($cache['checksum'])
+            || !isset($cache['@class'])
+            || $cache['checksum'] != $checksum
+            || $cache['@class'] != $class
+        ) {
+            // Attempt to lock the file for writing.
+            $file->lock(false);
+
+            // Load languages.
+            $this->languages = new Languages;
+            $pluginPaths = str_ireplace(GRAV_ROOT . '/', '', array_reverse($plugins));
+            foreach ($pluginPaths as $path) {
+                if (isset($languageFiles[$path])) {
+                    foreach ((array) $languageFiles[$path] as $plugin => $item) {
+                        $lang_file = CompiledYamlFile::instance($item['file']);
+                        $content = $lang_file->content();
+                        $this->languages->mergeRecursive($content);
+                    }
+                    unset($languageFiles[$path]);
+                }
+            }
+
+            foreach ($languageFiles as $location) {
+                foreach ($location as $lang => $item) {
+                    $lang_file = CompiledYamlFile::instance($item['file']);
+                    $content = $lang_file->content();
+                    $this->languages->join($lang, $content, '/');
+                }
+            }
+
+            $cache = [
+                '@class'   => $class,
+                'checksum' => $checksum,
+                'files'    => $languageFiles,
+                'data'     => $this->languages->toArray()
+            ];
+            // If compiled file wasn't already locked by another process, save it.
+            if ($file->locked() !== false) {
+                $this->messages[] = 'Saving compiled languages.';
+                $file->save($cache);
+                $file->unlock();
+            }
+        } else {
+            $this->languages = new Languages($cache['data']);
+        }
+    }
+
+    /**
+     * Load blueprints.
+     *
+     * @param array  $files
+     */
+    public function loadBlueprintFiles(array $files)
+    {
+        foreach ($files as $name => $item) {
+            $file = CompiledYamlFile::instance($item['file']);
+            $this->blueprints->embed($name, $file->content(), '/');
+        }
+    }
+
+    /**
+     * Load configuration.
+     *
+     * @param array  $files
+     */
+    public function loadConfigFiles(array $files)
+    {
+        foreach ($files as $name => $item) {
+            $file = CompiledYamlFile::instance($item['file']);
+            $this->join($name, $file->content(), '/');
+        }
+    }
+
+    /**
+     * Initialize resource locator by using the configuration.
+     *
+     * @param UniformResourceLocator $locator
+     */
+    public function initializeLocator(UniformResourceLocator $locator)
+    {
+        $locator->reset();
+
+        $schemes = (array) $this->get('streams.schemes', []);
+
+        foreach ($schemes as $scheme => $config) {
+            if (isset($config['paths'])) {
+                $locator->addPath($scheme, '', $config['paths']);
+            }
+            if (isset($config['prefixes'])) {
+                foreach ($config['prefixes'] as $prefix => $paths) {
+                    $locator->addPath($scheme, $prefix, $paths);
+                }
+            }
+        }
+    }
+
+    /**
+     * Get available streams and their types from the configuration.
+     *
+     * @return array
+     */
+    public function getStreams()
+    {
+        $schemes = [];
+        foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
+            $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
+            if ($type[0] != '\\') {
+                $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
+            }
+
+            $schemes[$scheme] = $type;
+        }
+
+        return $schemes;
+    }
+
+    public function getLanguages()
+    {
+        return $this->languages;
+    }
+}

+ 186 - 0
system/src/Grav/Common/Config/ConfigFinder.php

@@ -0,0 +1,186 @@
+<?php
+namespace Grav\Common\Config;
+
+use Grav\Common\Filesystem\Folder;
+
+/**
+ * The Configuration Finder class.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class ConfigFinder
+{
+    /**
+     * Get all locations for blueprint files (including plugins).
+     *
+     * @param array $blueprints
+     * @param array $plugins
+     * @return array
+     */
+    public function locateBlueprintFiles(array $blueprints, array $plugins)
+    {
+        $list = [];
+        foreach (array_reverse($plugins) as $folder) {
+            $list += $this->detectInFolder($folder, 'blueprints');
+        }
+        foreach (array_reverse($blueprints) as $folder) {
+            $list += $this->detectRecursive($folder);
+        }
+        return $list;
+    }
+
+    /**
+     * Get all locations for configuration files (including plugins).
+     *
+     * @param array $configs
+     * @param array $plugins
+     * @return array
+     */
+    public function locateConfigFiles(array $configs, array $plugins)
+    {
+        $list = [];
+        foreach (array_reverse($plugins) as $folder) {
+            $list += $this->detectInFolder($folder);
+        }
+        foreach (array_reverse($configs) as $folder) {
+            $list += $this->detectRecursive($folder);
+        }
+        return $list;
+    }
+
+    public function locateLanguageFiles(array $languages, array $plugins)
+    {
+        $list = [];
+        foreach (array_reverse($plugins) as $folder) {
+            $list += $this->detectLanguagesInFolder($folder, 'languages');
+        }
+        foreach (array_reverse($languages) as $folder) {
+            $list += $this->detectRecursive($folder);
+        }
+        return $list;
+    }
+
+    /**
+     * Get all locations for a single configuration file.
+     *
+     * @param  array  $folders Locations to look up from.
+     * @param  string $name    Filename to be located.
+     * @return array
+     */
+    public function locateConfigFile(array $folders, $name)
+    {
+        $filename = "{$name}.yaml";
+
+        $list = [];
+        foreach ($folders as $folder) {
+            $path = trim(Folder::getRelativePath($folder), '/');
+
+            if (is_file("{$folder}/{$filename}")) {
+                $modified = filemtime("{$folder}/{$filename}");
+            } else {
+                $modified = 0;
+            }
+            $list[$path] = [$name => ['file' => "{$path}/{$filename}", 'modified' => $modified]];
+        }
+
+        return $list;
+    }
+
+    /**
+     * Detects all plugins with a configuration file and returns them with last modification time.
+     *
+     * @param  string $folder Location to look up from.
+     * @param  string $lookup Filename to be located.
+     * @return array
+     * @internal
+     */
+    protected function detectInFolder($folder, $lookup = null)
+    {
+        $path = trim(Folder::getRelativePath($folder), '/');
+
+        $list = [];
+
+        if (is_dir($folder)) {
+            $iterator = new \FilesystemIterator($folder);
+
+            /** @var \DirectoryIterator $directory */
+            foreach ($iterator as $directory) {
+                if (!$directory->isDir()) {
+                    continue;
+                }
+
+                $name = $directory->getBasename();
+                $find = ($lookup ?: $name) . '.yaml';
+                $filename = "{$path}/{$name}/$find";
+
+                if (file_exists($filename)) {
+                    $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)];
+                }
+            }
+        }
+
+        return [$path => $list];
+    }
+
+    protected function detectLanguagesInFolder($folder, $lookup = null)
+    {
+        $path = trim(Folder::getRelativePath($folder), '/');
+
+        $list = [];
+
+        if (is_dir($folder)) {
+            $iterator = new \FilesystemIterator($folder);
+
+            /** @var \DirectoryIterator $directory */
+            foreach ($iterator as $directory) {
+                if (!$directory->isDir()) {
+                    continue;
+                }
+
+                $name = $directory->getBasename();
+                $find = ($lookup ?: $name) . '.yaml';
+                $filename = "{$path}/{$name}/$find";
+
+                if (file_exists($filename)) {
+                    $list[$name] = ['file' => $filename, 'modified' => filemtime($filename)];
+                }
+            }
+        }
+
+        return [$path => $list];
+    }
+
+    /**
+     * Detects all plugins with a configuration file and returns them with last modification time.
+     *
+     * @param  string $folder Location to look up from.
+     * @return array
+     * @internal
+     */
+    protected function detectRecursive($folder)
+    {
+        $path = trim(Folder::getRelativePath($folder), '/');
+
+        if (is_dir($folder)) {
+            // Find all system and user configuration files.
+            $options = [
+                'compare' => 'Filename',
+                'pattern' => '|\.yaml$|',
+                'filters' => [
+                    'key' => '|\.yaml$|',
+                    'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
+                        return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
+                    }
+                ],
+                'key' => 'SubPathname'
+            ];
+
+            $list = Folder::all($folder, $options);
+        } else {
+            $list = [];
+        }
+
+        return [$path => $list];
+    }
+}

+ 27 - 0
system/src/Grav/Common/Config/Languages.php

@@ -0,0 +1,27 @@
+<?php
+namespace Grav\Common\Config;
+
+use Grav\Common\Data\Data;
+
+/**
+ * The Languages class contains configuration rules.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Languages extends Data
+{
+
+    public function reformat()
+    {
+        if (isset($this->items['plugins'])) {
+            $this->items = array_merge_recursive($this->items, $this->items['plugins']);
+            unset($this->items['plugins']);
+        }
+    }
+
+    public function mergeRecursive(array $data)
+    {
+        $this->items = array_merge_recursive($this->items, $data);
+    }
+}

+ 456 - 0
system/src/Grav/Common/Data/Blueprint.php

@@ -0,0 +1,456 @@
+<?php
+namespace Grav\Common\Data;
+
+use Grav\Common\GravTrait;
+use RocketTheme\Toolbox\ArrayTraits\Export;
+
+/**
+ * Blueprint handles the inside logic of blueprints.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Blueprint
+{
+    use Export, DataMutatorTrait, GravTrait;
+
+    public $name;
+
+    public $initialized = false;
+
+    protected $items;
+    protected $context;
+    protected $fields;
+    protected $rules = array();
+    protected $nested = array();
+    protected $filter = ['validation' => 1];
+
+    /**
+     * @param string $name
+     * @param array  $data
+     * @param Blueprints $context
+     */
+    public function __construct($name, array $data = array(), Blueprints $context = null)
+    {
+        $this->name = $name;
+        $this->items = $data;
+        $this->context = $context;
+    }
+
+    /**
+     * Set filter for inherited properties.
+     *
+     * @param array $filter     List of field names to be inherited.
+     */
+    public function setFilter(array $filter)
+    {
+        $this->filter = array_flip($filter);
+    }
+
+    /**
+     * Return all form fields.
+     *
+     * @return array
+     */
+    public function fields()
+    {
+        if (!isset($this->fields)) {
+            $this->fields = [];
+            $this->embed('', $this->items);
+        }
+
+        return $this->fields;
+    }
+
+    /**
+     * Validate data against blueprints.
+     *
+     * @param  array $data
+     * @throws \RuntimeException
+     */
+    public function validate(array $data)
+    {
+        // Initialize data
+        $this->fields();
+
+        try {
+            $this->validateArray($data, $this->nested);
+        } catch (\RuntimeException $e) {
+            throw new \RuntimeException(sprintf('<b>Validation failed:</b> %s', $e->getMessage()));
+        }
+    }
+
+    /**
+     * Merge two arrays by using blueprints.
+     *
+     * @param  array $data1
+     * @param  array $data2
+     * @return array
+     */
+    public function mergeData(array $data1, array $data2)
+    {
+        // Initialize data
+        $this->fields();
+        return $this->mergeArrays($data1, $data2, $this->nested);
+    }
+
+    /**
+     * Filter data by using blueprints.
+     *
+     * @param  array $data
+     * @return array
+     */
+    public function filter(array $data)
+    {
+        // Initialize data
+        $this->fields();
+        return $this->filterArray($data, $this->nested);
+    }
+
+    /**
+     * Return data fields that do not exist in blueprints.
+     *
+     * @param  array  $data
+     * @param  string $prefix
+     * @return array
+     */
+    public function extra(array $data, $prefix = '')
+    {
+        // Initialize data
+        $this->fields();
+        $rules = $this->nested;
+
+        // Drill down to prefix level
+        if (!empty($prefix)) {
+            $parts = explode('.', trim($prefix, '.'));
+            foreach ($parts as $part) {
+                $rules = isset($rules[$part]) ? $rules[$part] : [];
+            }
+        }
+
+        return $this->extraArray($data, $rules, $prefix);
+    }
+
+    /**
+     * Extend blueprint with another blueprint.
+     *
+     * @param Blueprint $extends
+     * @param bool $append
+     */
+    public function extend(Blueprint $extends, $append = false)
+    {
+        $blueprints = $append ? $this->items : $extends->toArray();
+        $appended = $append ? $extends->toArray() : $this->items;
+
+        $bref_stack = array(&$blueprints);
+        $head_stack = array($appended);
+
+        do {
+            end($bref_stack);
+
+            $bref = &$bref_stack[key($bref_stack)];
+            $head = array_pop($head_stack);
+
+            unset($bref_stack[key($bref_stack)]);
+
+            foreach (array_keys($head) as $key) {
+                if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) {
+                    $bref_stack[] = &$bref[$key];
+                    $head_stack[] = $head[$key];
+                } else {
+                    $bref = array_merge($bref, array($key => $head[$key]));
+                }
+            }
+        } while (count($head_stack));
+
+        $this->items = $blueprints;
+    }
+
+    /**
+     * Convert object into an array.
+     *
+     * @return array
+     */
+    public function getState()
+    {
+        return ['name' => $this->name, 'items' => $this->items, 'rules' => $this->rules, 'nested' => $this->nested];
+    }
+
+    /**
+     * Embed an array to the blueprint.
+     *
+     * @param $name
+     * @param array $value
+     * @param string $separator
+     */
+    public function embed($name, array $value, $separator = '.')
+    {
+
+        if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
+            return;
+        }
+        // Initialize data
+        $this->fields();
+        $prefix = $name ? strtr($name, $separator, '.') . '.' : '';
+        $params = array_intersect_key($this->filter, $value);
+        $this->parseFormFields($value['form']['fields'], $params, $prefix, $this->fields);
+    }
+
+    /**
+     * @param array $data
+     * @param array $rules
+     * @throws \RuntimeException
+     * @internal
+     */
+    protected function validateArray(array $data, array $rules)
+    {
+        $this->checkRequired($data, $rules);
+
+        foreach ($data as $key => $field) {
+            $val = isset($rules[$key]) ? $rules[$key] : null;
+            $rule = is_string($val) ? $this->rules[$val] : null;
+
+            if ($rule) {
+                // Item has been defined in blueprints.
+                Validation::validate($field, $rule);
+            } elseif (is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $this->validateArray($field, $val);
+            } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
+                 // Undefined/extra item.
+                 throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
+            }
+        }
+    }
+
+    /**
+     * @param array $data
+     * @param array $rules
+     * @return array
+     * @internal
+     */
+    protected function filterArray(array $data, array $rules)
+    {
+        $results = array();
+        foreach ($data as $key => $field) {
+            $val = isset($rules[$key]) ? $rules[$key] : null;
+            $rule = is_string($val) ? $this->rules[$val] : null;
+
+            if ($rule) {
+                // Item has been defined in blueprints.
+                if (is_array($field) && count($field) == 1 && reset($field) == '') {
+                    continue;
+                }
+                $field = Validation::filter($field, $rule);
+            } elseif (is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $field = $this->filterArray($field, $val);
+            } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
+                $field = null;
+            }
+
+            if (isset($field) && (!is_array($field) || !empty($field))) {
+                $results[$key] = $field;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * @param array $data1
+     * @param array $data2
+     * @param array $rules
+     * @return array
+     * @internal
+     */
+    protected function mergeArrays(array $data1, array $data2, array $rules)
+    {
+        foreach ($data2 as $key => $field) {
+            $val = isset($rules[$key]) ? $rules[$key] : null;
+            $rule = is_string($val) ? $this->rules[$val] : null;
+
+            if (!$rule && array_key_exists($key, $data1) && is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
+            } else {
+                // Otherwise just take value from the data2.
+                $data1[$key] = $field;
+            }
+        }
+
+        return $data1;
+    }
+
+    /**
+     * @param array $data
+     * @param array $rules
+     * @param string $prefix
+     * @return array
+     * @internal
+     */
+    protected function extraArray(array $data, array $rules, $prefix)
+    {
+        $array = array();
+        foreach ($data as $key => $field) {
+            $val = isset($rules[$key]) ? $rules[$key] : null;
+            $rule = is_string($val) ? $this->rules[$val] : null;
+
+            if ($rule) {
+                // Item has been defined in blueprints.
+            } elseif (is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $array += $this->ExtraArray($field, $val, $prefix . $key . '.');
+            } else {
+                // Undefined/extra item.
+                $array[$prefix.$key] = $field;
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * Gets all field definitions from the blueprints.
+     *
+     * @param array $fields
+     * @param array $params
+     * @param string $prefix
+     * @param array $current
+     * @internal
+     */
+    protected function parseFormFields(array &$fields, $params, $prefix, array &$current)
+    {
+        // Go though all the fields in current level.
+        foreach ($fields as $key => &$field) {
+            $current[$key] = &$field;
+            // Set name from the array key.
+            $field['name'] = $prefix . $key;
+            $field += $params;
+
+            if (isset($field['fields']) && (!isset($field['type']) || $field['type'] !== 'list')) {
+                // Recursively get all the nested fields.
+                $newParams = array_intersect_key($this->filter, $field);
+                $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']);
+            } else if ($field['type'] !== 'ignore') {
+                // Add rule.
+                $this->rules[$prefix . $key] = &$field;
+                $this->addProperty($prefix . $key);
+
+                foreach ($field as $name => $value) {
+                    // Support nested blueprints.
+                    if ($this->context && $name == '@import') {
+                        $values = (array) $value;
+                        if (!isset($field['fields'])) {
+                            $field['fields'] = array();
+                        }
+                        foreach ($values as $bname) {
+                            $b = $this->context->get($bname);
+                            $field['fields'] = array_merge($field['fields'], $b->fields());
+                        }
+                    }
+
+                    // Support for callable data values.
+                    elseif (substr($name, 0, 6) == '@data-') {
+                        $property = substr($name, 6);
+                        if (is_array($value)) {
+                            $func = array_shift($value);
+                        } else {
+                            $func = $value;
+                            $value = array();
+                        }
+                        list($o, $f) = preg_split('/::/', $func);
+                        if (!$f && function_exists($o)) {
+                            $data = call_user_func_array($o, $value);
+                        } elseif ($f && method_exists($o, $f)) {
+                            $data = call_user_func_array(array($o, $f), $value);
+                        }
+
+                        // If function returns a value,
+                        if (isset($data)) {
+                            if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
+                                // Combine field and @data-field together.
+                                $field[$property] += $data;
+                            } else {
+                                // Or create/replace field with @data-field.
+                                $field[$property] = $data;
+                            }
+                        }
+                    }
+
+                    elseif (substr($name, 0, 8) == '@config-') {
+                        $property = substr($name, 8);
+                        $default = isset($field[$property]) ? $field[$property] : null;
+                        $config = self::getGrav()['config']->get($value, $default);
+
+                        if (!is_null($config)) {
+                            $field[$property] = $config;
+                        }
+                    }
+                }
+
+                // Initialize predefined validation rule.
+                if (isset($field['validate']['rule']) && $field['type'] !== 'ignore') {
+                    $field['validate'] += $this->getRule($field['validate']['rule']);
+                }
+            }
+        }
+    }
+
+    /**
+     * Add property to the definition.
+     *
+     * @param  string  $path  Comma separated path to the property.
+     * @internal
+     */
+    protected function addProperty($path)
+    {
+        $parts = explode('.', $path);
+        $item = array_pop($parts);
+
+        $nested = &$this->nested;
+        foreach ($parts as $part) {
+            if (!isset($nested[$part])) {
+                $nested[$part] = array();
+            }
+            $nested = &$nested[$part];
+        }
+
+        if (!isset($nested[$item])) {
+            $nested[$item] = $path;
+        }
+    }
+
+    /**
+     * @param $rule
+     * @return array
+     * @internal
+     */
+    protected function getRule($rule)
+    {
+        if (isset($this->items['rules'][$rule]) && is_array($this->items['rules'][$rule])) {
+            return $this->items['rules'][$rule];
+        }
+        return array();
+    }
+
+    /**
+     * @param array $data
+     * @param array $fields
+     * @throws \RuntimeException
+     * @internal
+     */
+    protected function checkRequired(array $data, array $fields)
+    {
+        foreach ($fields as $name => $field) {
+            if (!is_string($field)) {
+                continue;
+            }
+            $field = $this->rules[$field];
+            if (isset($field['validate']['required'])
+                && $field['validate']['required'] === true
+                && empty($data[$name])) {
+                throw new \RuntimeException("Missing required field: {$field['name']}");
+            }
+        }
+    }
+}

+ 145 - 0
system/src/Grav/Common/Data/Blueprints.php

@@ -0,0 +1,145 @@
+<?php
+namespace Grav\Common\Data;
+
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\GravTrait;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+/**
+ * Blueprints class keeps track on blueprint instances.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Blueprints
+{
+    use GravTrait;
+
+    protected $search;
+    protected $types;
+    protected $instances = array();
+
+    /**
+     * @param  string|array  $search  Search path.
+     */
+    public function __construct($search)
+    {
+        $this->search = $search;
+    }
+
+    /**
+     * Get blueprint.
+     *
+     * @param  string  $type  Blueprint type.
+     * @return Blueprint
+     * @throws \RuntimeException
+     */
+    public function get($type)
+    {
+        if (!isset($this->instances[$type])) {
+            $parents = [];
+            if (is_string($this->search)) {
+                $filename = $this->search . $type . YAML_EXT;
+
+                // Check if search is a stream and resolve the path.
+                if (strpos($filename, '://')) {
+                    $grav = static::getGrav();
+                    /** @var UniformResourceLocator $locator */
+                    $locator = $grav['locator'];
+                    $parents = $locator->findResources($filename);
+                    $filename = array_shift($parents);
+                }
+            } else {
+                $filename = isset($this->search[$type]) ? $this->search[$type] : '';
+            }
+
+            if ($filename && is_file($filename)) {
+                $file = CompiledYamlFile::instance($filename);
+                $blueprints = $file->content();
+            } else {
+                $blueprints = [];
+            }
+
+            $blueprint = new Blueprint($type, $blueprints, $this);
+
+            if (isset($blueprints['@extends'])) {
+                // Extend blueprint by other blueprints.
+                $extends = (array) $blueprints['@extends'];
+
+                if (is_string(key($extends))) {
+                    $extends = [ $extends ];
+                }
+
+                foreach ($extends as $extendConfig) {
+                    $extendType = !is_string($extendConfig) ? empty($extendConfig['type']) ? false : $extendConfig['type'] : $extendConfig;
+
+                    if (!$extendType) {
+                        continue;
+                    } elseif ($extendType === '@parent') {
+                        $parentFile = array_shift($parents);
+                        if (!$parentFile || !is_file($parentFile)) {
+                            continue;
+                        }
+                        $blueprints = CompiledYamlFile::instance($parentFile)->content();
+                        $parent = new Blueprint($type.'-parent', $blueprints, $this);
+                        $blueprint->extend($parent);
+                        continue;
+                    }
+
+                    if (is_string($extendConfig) || empty($extendConfig['context'])) {
+                        $context = $this;
+                    } else {
+                        // Load blueprints from external context.
+                        $array = explode('://', $extendConfig['context'], 2);
+                        $scheme = array_shift($array);
+                        $path = array_shift($array);
+                        if ($path) {
+                            $scheme .= '://';
+                            $extendType = $path ? "{$path}/{$extendType}" : $extendType;
+                        }
+                        $context = new self($scheme);
+                    }
+                    $blueprint->extend($context->get($extendType));
+                }
+            }
+
+            $this->instances[$type] = $blueprint;
+        }
+
+        return $this->instances[$type];
+    }
+
+    /**
+     * Get all available blueprint types.
+     *
+     * @return  array  List of type=>name
+     */
+    public function types()
+    {
+        if ($this->types === null) {
+            $this->types = array();
+
+            // Check if search is a stream.
+            if (strpos($this->search, '://')) {
+                // Stream: use UniformResourceIterator.
+                $grav = static::getGrav();
+                /** @var UniformResourceLocator $locator */
+                $locator = $grav['locator'];
+                $iterator = $locator->getIterator($this->search, null);
+            } else {
+                // Not a stream: use DirectoryIterator.
+                $iterator = new \DirectoryIterator($this->search);
+            }
+
+            /** @var \DirectoryIterator $file */
+            foreach ($iterator as $file) {
+                if (!$file->isFile() || '.' . $file->getExtension() != YAML_EXT) {
+                    continue;
+                }
+                $name = $file->getBasename(YAML_EXT);
+                $this->types[$name] = ucfirst(strtr($name, '_', ' '));
+            }
+        }
+        return $this->types;
+    }
+}

+ 240 - 0
system/src/Grav/Common/Data/Data.php

@@ -0,0 +1,240 @@
+<?php
+namespace Grav\Common\Data;
+
+use RocketTheme\Toolbox\ArrayTraits\ArrayAccessWithGetters;
+use RocketTheme\Toolbox\ArrayTraits\Countable;
+use RocketTheme\Toolbox\ArrayTraits\Export;
+use RocketTheme\Toolbox\File\File;
+use RocketTheme\Toolbox\File\FileInterface;
+
+/**
+ * Recursive data object
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Data implements DataInterface
+{
+    use ArrayAccessWithGetters, Countable, Export, DataMutatorTrait;
+
+    protected $gettersVariable = 'items';
+    protected $items;
+
+    /**
+     * @var Blueprints
+     */
+    protected $blueprints;
+
+    /**
+     * @var File
+     */
+    protected $storage;
+
+    /**
+     * @param array $items
+     * @param Blueprint $blueprints
+     */
+    public function __construct(array $items = array(), Blueprint $blueprints = null)
+    {
+        $this->items = $items;
+
+        $this->blueprints = $blueprints;
+    }
+
+    /**
+     * Get value by using dot notation for nested arrays/objects.
+     *
+     * @example $value = $data->value('this.is.my.nested.variable');
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $default    Default value (or null).
+     * @param string  $separator  Separator, defaults to '.'
+     * @return mixed  Value.
+     */
+    public function value($name, $default = null, $separator = '.')
+    {
+        return $this->get($name, $default, $separator);
+    }
+
+    /**
+     * Set default value by using dot notation for nested arrays/objects.
+     *
+     * @example $data->def('this.is.my.nested.variable', 'default');
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $default    Default value (or null).
+     * @param string  $separator  Separator, defaults to '.'
+     */
+    public function def($name, $default = null, $separator = '.')
+    {
+        $this->set($name, $this->get($name, $default, $separator), $separator);
+    }
+
+    /**
+     * Join two values together by using blueprints if available.
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $value      Value to be joined.
+     * @param string  $separator  Separator, defaults to '.'
+     */
+    public function join($name, $value, $separator = '.')
+    {
+        $old = $this->get($name, null, $separator);
+        if ($old === null) {
+            // Variable does not exist yet: just use the incoming value.
+        } elseif ($this->blueprints) {
+            // Blueprints: join values by using blueprints.
+            $value = $this->blueprints->mergeData($old, $value, $name, $separator);
+        } else {
+            // No blueprints: replace existing top level variables with the new ones.
+            $value = array_merge($old, $value);
+        }
+
+        $this->set($name, $value, $separator);
+    }
+
+    /**
+     * Join two values together by using blueprints if available.
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $value      Value to be joined.
+     * @param string  $separator  Separator, defaults to '.'
+     */
+    public function joinDefaults($name, $value, $separator = '.')
+    {
+        $old = $this->get($name, null, $separator);
+        if ($old === null) {
+            // Variable does not exist yet: just use the incoming value.
+        } elseif ($this->blueprints) {
+            // Blueprints: join values by using blueprints.
+            $value = $this->blueprints->mergeData($value, $old, $name, $separator);
+        } else {
+            // No blueprints: replace existing top level variables with the new ones.
+            $value = array_merge($value, $old);
+        }
+
+        $this->set($name, $value, $separator);
+    }
+
+
+    /**
+     * Merge two sets of data together.
+     *
+     * @param array $data
+     * @return void
+     */
+    public function merge(array $data)
+    {
+        if ($this->blueprints) {
+            $this->items = $this->blueprints->mergeData($this->items, $data);
+        } else {
+            $this->items = array_merge($this->items, $data);
+        }
+    }
+
+    /**
+     * Add default data to the set.
+     *
+     * @param array $data
+     * @return void
+     */
+    public function setDefaults(array $data)
+    {
+        if ($this->blueprints) {
+            $this->items = $this->blueprints->mergeData($data, $this->items);
+        } else {
+            $this->items = array_merge($data, $this->items);
+        }
+    }
+
+    /**
+     * Return blueprints.
+     *
+     * @return Blueprint
+     */
+    public function blueprints()
+    {
+        return $this->blueprints;
+    }
+
+    /**
+     * Validate by blueprints.
+     *
+     * @throws \Exception
+     */
+    public function validate()
+    {
+        if ($this->blueprints) {
+            $this->blueprints->validate($this->items);
+        }
+    }
+
+    /**
+     * Filter all items by using blueprints.
+     */
+    public function filter()
+    {
+        if ($this->blueprints) {
+            $this->items = $this->blueprints->filter($this->items);
+        }
+    }
+
+    /**
+     * Get extra items which haven't been defined in blueprints.
+     *
+     * @return array
+     */
+    public function extra()
+    {
+        return $this->blueprints ? $this->blueprints->extra($this->items) : array();
+    }
+
+    /**
+     * Save data if storage has been defined.
+     */
+    public function save()
+    {
+        $file = $this->file();
+        if ($file) {
+            $file->save($this->items);
+        }
+    }
+
+    /**
+     * Returns whether the data already exists in the storage.
+     *
+     * NOTE: This method does not check if the data is current.
+     *
+     * @return bool
+     */
+    public function exists()
+    {
+        return $this->file()->exists();
+    }
+
+    /**
+     * Return unmodified data as raw string.
+     *
+     * NOTE: This function only returns data which has been saved to the storage.
+     *
+     * @return string
+     */
+    public function raw()
+    {
+        return $this->file()->raw();
+    }
+
+    /**
+     * Set or get the data storage.
+     *
+     * @param FileInterface $storage Optionally enter a new storage.
+     * @return FileInterface
+     */
+    public function file(FileInterface $storage = null)
+    {
+        if ($storage) {
+            $this->storage = $storage;
+        }
+        return $this->storage;
+    }
+}

+ 68 - 0
system/src/Grav/Common/Data/DataInterface.php

@@ -0,0 +1,68 @@
+<?php
+namespace Grav\Common\Data;
+
+use RocketTheme\Toolbox\File\FileInterface;
+
+/**
+ * Data interface
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+interface DataInterface
+{
+    /**
+     * Get value by using dot notation for nested arrays/objects.
+     *
+     * @example $value = $data->value('this.is.my.nested.variable');
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $default    Default value (or null).
+     * @param string  $separator  Separator, defaults to '.'
+     * @return mixed  Value.
+     */
+    public function value($name, $default = null, $separator = '.');
+
+    /**
+     * Merge external data.
+     *
+     * @param array $data
+     * @return mixed
+     */
+    public function merge(array $data);
+
+    /**
+     * Return blueprints.
+     */
+    public function blueprints();
+
+    /**
+     * Validate by blueprints.
+     *
+     * @throws \Exception
+     */
+    public function validate();
+
+    /**
+     * Filter all items by using blueprints.
+     */
+    public function filter();
+
+    /**
+     * Get extra items which haven't been defined in blueprints.
+     */
+    public function extra();
+
+    /**
+     * Save data into the file.
+     */
+    public function save();
+
+    /**
+     * Set or get the data storage.
+     *
+     * @param FileInterface $storage Optionally enter a new storage.
+     * @return FileInterface
+     */
+    public function file(FileInterface $storage = null);
+}

+ 68 - 0
system/src/Grav/Common/Data/DataMutatorTrait.php

@@ -0,0 +1,68 @@
+<?php
+namespace Grav\Common\Data;
+
+trait DataMutatorTrait
+{
+
+    /**
+     * Get value by using dot notation for nested arrays/objects.
+     *
+     * @example $value = $data->get('this.is.my.nested.variable');
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $default    Default value (or null).
+     * @param string  $separator  Separator, defaults to '.'
+     * @return mixed  Value.
+     */
+    public function get($name, $default = null, $separator = '.')
+    {
+        $path = explode($separator, $name);
+        $current = $this->items;
+        foreach ($path as $field) {
+            if (is_object($current) && isset($current->{$field})) {
+                $current = $current->{$field};
+            } elseif (is_array($current) && isset($current[$field])) {
+                $current = $current[$field];
+            } else {
+                return $default;
+            }
+        }
+
+        return $current;
+    }
+
+    /**
+     * Set value by using dot notation for nested arrays/objects.
+     *
+     * @example $value = $data->set('this.is.my.nested.variable', true);
+     *
+     * @param string  $name       Dot separated path to the requested value.
+     * @param mixed   $value      New value.
+     * @param string  $separator  Separator, defaults to '.'
+     */
+    public function set($name, $value, $separator = '.')
+    {
+        $path = explode($separator, $name);
+        $current = &$this->items;
+        foreach ($path as $field) {
+            if (is_object($current)) {
+                // Handle objects.
+                if (!isset($current->{$field})) {
+                    $current->{$field} = array();
+                }
+                $current = &$current->{$field};
+            } else {
+                // Handle arrays and scalars.
+                if (!is_array($current)) {
+                    $current = array($field => array());
+                } elseif (!isset($current[$field])) {
+                    $current[$field] = array();
+                }
+                $current = &$current[$field];
+            }
+        }
+
+        $current = $value;
+    }
+
+}

+ 672 - 0
system/src/Grav/Common/Data/Validation.php

@@ -0,0 +1,672 @@
+<?php
+namespace Grav\Common\Data;
+use Grav\Common\GravTrait;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Parser;
+
+/**
+ * Data validation.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+class Validation
+{
+    use GravTrait;
+
+    /**
+     * Validate value against a blueprint field definition.
+     *
+     * @param mixed $value
+     * @param array $field
+     * @throws \RuntimeException
+     */
+    public static function validate($value, array $field)
+    {
+        $validate = isset($field['validate']) ? (array) $field['validate'] : array();
+
+        // If value isn't required, we will stop validation if empty value is given.
+        if (empty($validate['required']) && ($value === null || $value === '')) {
+            return;
+        }
+
+        // Get language class
+        $language = self::getGrav()['language'];
+
+        // Validate type with fallback type text.
+        $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
+        $method = 'type'.strtr($type, '-', '_');
+        $name = ucfirst(isset($field['label']) ? $field['label'] : $field['name']);
+        $message = (string) isset($field['validate']['message']) ? $field['validate']['message'] : 'Invalid input in "' . $language->translate($name) . '""';
+
+        if (method_exists(__CLASS__, $method)) {
+            $success = self::$method($value, $validate, $field);
+        } else {
+            $success = self::typeText($value, $validate, $field);
+        }
+        if (!$success) {
+            throw new \RuntimeException($message);
+        }
+
+        // Check individual rules
+        foreach ($validate as $rule => $params) {
+            $method = 'validate'.strtr($rule, '-', '_');
+            if (method_exists(__CLASS__, $method)) {
+                $success = self::$method($value, $params);
+
+                if (!$success) {
+                    throw new \RuntimeException($message);
+                }
+            }
+        }
+    }
+
+    /**
+     * Filter value against a blueprint field definition.
+     *
+     * @param  mixed  $value
+     * @param  array  $field
+     * @return mixed  Filtered value.
+     */
+    public static function filter($value, array $field)
+    {
+        $validate = isset($field['validate']) ? (array) $field['validate'] : array();
+
+        // If value isn't required, we will return null if empty value is given.
+        if (empty($validate['required']) && ($value === null || $value === '')) {
+            return null;
+        }
+
+        // if this is a YAML field, simply parse it and return the value
+        if (isset($field['yaml']) && $field['yaml'] === true) {
+            try {
+                $yaml = new Parser();
+                return $yaml->parse($value);
+            } catch (ParseException $e) {
+                throw new \RuntimeException($e->getMessage());
+            }
+        }
+
+        // Validate type with fallback type text.
+        $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
+        $method = 'filter'.strtr($type, '-', '_');
+        if (method_exists(__CLASS__, $method)) {
+            $value = self::$method($value, $validate, $field);
+        } else {
+            $value = self::filterText($value, $validate, $field);
+        }
+
+        return $value;
+    }
+
+    /**
+     * HTML5 input: text
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeText($value, array $params, array $field)
+    {
+        if (!is_string($value)) {
+            return false;
+        }
+
+        if (isset($params['min']) && strlen($value) < $params['min']) {
+            return false;
+        }
+
+        if (isset($params['max']) && strlen($value) > $params['max']) {
+            return false;
+        }
+
+        $min = isset($params['min']) ? $params['min'] : 0;
+        if (isset($params['step']) && (strlen($value) - $min) % $params['step'] == 0) {
+            return false;
+        }
+
+        if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected static function filterText($value, array $params, array $field)
+    {
+        return (string) $value;
+    }
+
+    protected static function filterCommaList($value, array $params, array $field)
+    {
+        return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+    }
+
+    protected static function typeCommaList($value, array $params, array $field)
+    {
+        return is_array($value) ? true : self::typeText($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: textarea
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeTextarea($value, array $params, array $field)
+    {
+        if (!isset($params['multiline'])) {
+            $params['multiline'] = true;
+        }
+
+        return self::typeText($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: password
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typePassword($value, array $params, array $field)
+    {
+        return self::typeText($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: hidden
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeHidden($value, array $params, array $field)
+    {
+        return self::typeText($value, $params, $field);
+    }
+
+    /**
+     * Custom input: checkbox list
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeCheckboxes($value, array $params, array $field)
+    {
+        return self::typeArray((array) $value, $params, $field);
+    }
+
+    protected static function filterCheckboxes($value, array $params, array $field)
+    {
+        return self::filterArray($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: checkbox
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeCheckbox($value, array $params, array $field)
+    {
+        $value = (string) $value;
+
+        if (!isset($field['value'])) {
+            $field['value'] = 1;
+        }
+        if ($value && $value != $field['value']) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * HTML5 input: radio
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeRadio($value, array $params, array $field)
+    {
+        return self::typeArray((array) $value, $params, $field);
+    }
+
+    /**
+     * Custom input: toggle
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeToggle($value, array $params, array $field)
+    {
+        return self::typeArray((array) $value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: select
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeSelect($value, array $params, array $field)
+    {
+        return self::typeArray((array) $value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: number
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+
+    public static function typeNumber($value, array $params, array $field)
+    {
+        if (!is_numeric($value)) {
+            return false;
+        }
+
+        if (isset($params['min']) && $value < $params['min']) {
+            return false;
+        }
+
+        if (isset($params['max']) && $value > $params['max']) {
+            return false;
+        }
+
+        $min = isset($params['min']) ? $params['min'] : 0;
+        if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected static function filterNumber($value, array $params, array $field)
+    {
+        return (int) $value;
+    }
+
+    protected static function filterDateTime($value, array $params, array $field)
+    {
+        $format = self::getGrav()['config']->get('system.pages.dateformat.default');
+        if ($format) {
+            $converted = new \DateTime($value);
+            return $converted->format($format);
+        }
+        return $value;
+    }
+
+
+    /**
+     * HTML5 input: range
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeRange($value, array $params, array $field)
+    {
+        return self::typeNumber($value, $params, $field);
+    }
+
+    protected static function filterRange($value, array $params, array $field)
+    {
+        return self::filterNumber($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: color
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeColor($value, array $params, array $field)
+    {
+        return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
+    }
+
+    /**
+     * HTML5 input: email
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeEmail($value, array $params, array $field)
+    {
+        return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL);
+    }
+
+    /**
+     * HTML5 input: url
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+
+    public static function typeUrl($value, array $params, array $field)
+    {
+        return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
+    }
+
+    /**
+     * HTML5 input: datetime
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeDatetime($value, array $params, array $field)
+    {
+        if ($value instanceof \DateTime) {
+            return true;
+        } elseif (!is_string($value)) {
+            return false;
+        } elseif (!isset($params['format'])) {
+            return false !== strtotime($value);
+        }
+
+        $dateFromFormat = \DateTime::createFromFormat($params['format'], $value);
+
+        return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
+    }
+
+    /**
+     * HTML5 input: datetime-local
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeDatetimeLocal($value, array $params, array $field)
+    {
+        return self::typeDatetime($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: date
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeDate($value, array $params, array $field)
+    {
+        $params = array($params);
+        if (!isset($params['format'])) {
+            $params['format'] = 'Y-m-d';
+        }
+        return self::typeDatetime($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: time
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeTime($value, array $params, array $field)
+    {
+        $params = array($params);
+        if (!isset($params['format'])) {
+            $params['format'] = 'H:i';
+        }
+        return self::typeDatetime($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: month
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeMonth($value, array $params, array $field)
+    {
+        $params = array($params);
+        if (!isset($params['format'])) {
+            $params['format'] = 'Y-m';
+        }
+        return self::typeDatetime($value, $params, $field);
+    }
+
+    /**
+     * HTML5 input: week
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeWeek($value, array $params, array $field)
+    {
+        if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
+            return false;
+        }
+        return self::typeDatetime($value, $params, $field);
+    }
+
+    /**
+     * Custom input: array
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeArray($value, array $params, array $field)
+    {
+        if (!is_array($value)) {
+            return false;
+        }
+
+        if (isset($field['multiple'])) {
+            if (isset($params['min']) && count($value) < $params['min']) {
+                return false;
+            }
+
+            if (isset($params['max']) && count($value) > $params['max']) {
+                return false;
+            }
+
+            $min = isset($params['min']) ? $params['min'] : 0;
+            if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) {
+                return false;
+            }
+        }
+
+        $options = isset($field['options']) ? array_keys($field['options']) : array();
+        $values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value;
+        if ($options && array_diff($values, $options)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected static function filterArray($value, $params, $field)
+    {
+        $values = (array) $value;
+        $options = isset($field['options']) ? array_keys($field['options']) : array();
+        $multi = isset($field['multiple']) ? $field['multiple'] : false;
+
+        if ($options) {
+            $useKey = isset($field['use']) && $field['use'] == 'keys';
+            foreach ($values as $key => $value) {
+                $values[$key] = $useKey ? (bool) $value : $value;
+            }
+        }
+
+        if ($multi) {
+            foreach ($values as $key => $value) {
+                if (is_array($value)) {
+                    $value = implode(',', $value);
+                }
+
+                $values[$key] =  array_map('trim', explode(',', $value));
+            }
+        }
+
+        return $values;
+    }
+
+    public static function typeList($value, array $params, array $field)
+    {
+        if (!is_array($value)) {
+            return false;
+        }
+
+        if (isset($field['fields'])) {
+            foreach ($value as $key => $item) {
+                foreach ($field['fields'] as $subKey => $subField) {
+                    $subKey = trim($subKey, '.');
+                    $subValue = isset($item[$subKey]) ? $item[$subKey] : null;
+                    self::validate($subValue, $subField);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    protected static function filterList($value, array $params, array $field)
+    {
+        return (array) $value;
+    }
+
+    /**
+     * Custom input: ignore (will not validate)
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeIgnore($value, array $params, array $field)
+    {
+        return true;
+    }
+
+    public static function filterIgnore($value, array $params, array $field)
+    {
+        return $value;
+    }
+
+    // HTML5 attributes (min, max and range are handled inside the types)
+
+    public static function validateRequired($value, $params)
+    {
+        if (is_string($value)) {
+            $value = trim($value);
+        }
+        
+        return (bool) $params !== true || !empty($value);
+    }
+
+    public static function validatePattern($value, $params)
+    {
+        return (bool) preg_match("`^{$params}$`u", $value);
+    }
+
+
+    // Internal types
+
+    public static function validateAlpha($value, $params)
+    {
+        return ctype_alpha($value);
+    }
+
+    public static function validateAlnum($value, $params)
+    {
+        return ctype_alnum($value);
+    }
+
+    public static function typeBool($value, $params)
+    {
+        return is_bool($value) || $value == 1 || $value == 0;
+    }
+
+    public static function validateBool($value, $params)
+    {
+        return is_bool($value) || $value == 1 || $value == 0;
+    }
+
+    protected static function filterBool($value, $params)
+    {
+        return (bool) $value;
+    }
+
+    public static function validateDigit($value, $params)
+    {
+        return ctype_digit($value);
+    }
+
+    public static function validateFloat($value, $params)
+    {
+        return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
+    }
+
+    protected static function filterFloat($value, $params)
+    {
+        return (float) $value;
+    }
+
+    public static function validateHex($value, $params)
+    {
+        return ctype_xdigit($value);
+    }
+
+    public static function validateInt($value, $params)
+    {
+        return is_numeric($value) && (int) $value == $value;
+    }
+
+    protected static function filterInt($value, $params)
+    {
+        return (int) $value;
+    }
+
+    public static function validateArray($value, $params)
+    {
+        return is_array($value) || ($value instanceof \ArrayAccess
+            && $value instanceof \Traversable
+            && $value instanceof \Countable);
+    }
+
+    public static function validateJson($value, $params)
+    {
+        return (bool) (json_decode($value));
+    }
+}

+ 121 - 0
system/src/Grav/Common/Debugger.php

@@ -0,0 +1,121 @@
+<?php
+namespace Grav\Common;
+
+use DebugBar\JavascriptRenderer;
+use DebugBar\StandardDebugBar;
+
+/**
+ * Class Debugger
+ * @package Grav\Common
+ */
+class Debugger
+{
+    protected $grav;
+    protected $debugbar;
+    protected $renderer;
+    protected $enabled;
+
+    public function __construct()
+    {
+        $this->debugbar = new StandardDebugBar();
+        $this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
+    }
+
+    public function init()
+    {
+        $this->grav = Grav::instance();
+
+        if ($this->enabled()) {
+            $this->debugbar->addCollector(new \DebugBar\DataCollector\ConfigCollector((array)$this->grav['config']->get('system')));
+        }
+        return $this;
+    }
+
+    public function enabled($state = null)
+    {
+        if (isset($state)) {
+            $this->enabled = $state;
+        } else {
+            if (!isset($this->enabled)) {
+                $this->enabled = $this->grav['config']->get('system.debugger.enabled');
+            }
+        }
+        return $this->enabled;
+    }
+
+    public function addAssets()
+    {
+        if ($this->enabled()) {
+            $assets = $this->grav['assets'];
+
+            // Add jquery library
+            $assets->add('jquery', 101);
+
+            $this->renderer = $this->debugbar->getJavascriptRenderer();
+            $this->renderer->setIncludeVendors(false);
+
+            // Get the required CSS files
+            list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
+            foreach ($css_files as $css) {
+                $assets->addCss($css);
+            }
+
+            $assets->addCss('/system/assets/debugger.css');
+
+            foreach ($js_files as $js) {
+                $assets->addJs($js);
+            }
+        }
+        return $this;
+    }
+
+    public function addCollector($collector)
+    {
+        $this->debugbar->addCollector($collector);
+        return $this;
+    }
+
+    public function getCollector($collector)
+    {
+        return $this->debugbar->getCollector($collector);
+    }
+
+    public function render()
+    {
+        if ($this->enabled()) {
+            echo $this->renderer->render();
+        }
+        return $this;
+    }
+
+    public function sendDataInHeaders()
+    {
+        $this->debugbar->sendDataInHeaders();
+        return $this;
+    }
+
+    public function startTimer($name, $description = null)
+    {
+        if ($name[0] == '_' || $this->grav['config']->get('system.debugger.enabled')) {
+            $this->debugbar['time']->startMeasure($name, $description);
+        }
+        return $this;
+    }
+
+    public function stopTimer($name)
+    {
+        if ($name[0] == '_' || $this->grav['config']->get('system.debugger.enabled')) {
+            $this->debugbar['time']->stopMeasure($name);
+        }
+        return $this;
+    }
+
+
+    public function addMessage($message, $label = 'info', $isString = true)
+    {
+        if ($this->enabled()) {
+            $this->debugbar['messages']->addMessage($message, $label, $isString);
+        }
+        return $this;
+    }
+}

+ 52 - 0
system/src/Grav/Common/Errors/Errors.php

@@ -0,0 +1,52 @@
+<?php
+namespace Grav\Common\Errors;
+
+use Grav\Common\Grav;
+use Whoops\Handler\CallbackHandler;
+use Whoops\Handler\HandlerInterface;
+use Whoops\Run;
+
+/**
+ * Class Debugger
+ * @package Grav\Common
+ */
+class Errors extends \Whoops\Run
+{
+
+    public function pushHandler($handler, $key = null)
+    {
+        if (is_callable($handler)) {
+            $handler = new CallbackHandler($handler);
+        }
+
+        if (!$handler instanceof HandlerInterface) {
+            throw new \InvalidArgumentException(
+                "Argument to " . __METHOD__ . " must be a callable, or instance of"
+                . "Whoops\\Handler\\HandlerInterface"
+            );
+        }
+
+        // Store with key if provided
+        if ($key) {
+            $this->handlerStack[$key] = $handler;
+        } else {
+            $this->handlerStack[] = $handler;
+        }
+
+        return $this;
+    }
+
+    public function resetHandlers()
+    {
+        $grav = Grav::instance();
+        $config = $grav['config']->get('system.errors');
+        if (isset($config['display']) && !$config['display']) {
+            unset($this->handlerStack['pretty']);
+            $this->handlerStack = array('simple' => new SimplePageHandler()) + $this->handlerStack;
+        }
+        if (isset($config['log']) && !$config['log']) {
+            unset($this->handlerStack['log']);
+        }
+    }
+
+}

+ 52 - 0
system/src/Grav/Common/Errors/Resources/error.css

@@ -0,0 +1,52 @@
+html, body {
+    height: 100%
+}
+body {
+    margin:0 3rem;
+    padding:0;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-size: 1.5rem;
+    line-height: 1.4;
+    display: -webkit-box;      /* OLD - iOS 6-, Safari 3.1-6 */
+    display: -moz-box;         /* OLD - Firefox 19- (buggy but mostly works) */
+    display: -ms-flexbox;      /* TWEENER - IE 10 */
+    display: -webkit-flex;     /* NEW - Chrome */
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: center;
+    justify-content: center;
+}
+.container {
+    margin: 0rem;
+    max-width: 600px;
+    padding-bottom:5rem;
+}
+
+header {
+    color: #000;
+    font-size: 4rem;
+    letter-spacing: 2px;
+    line-height: 1.1;
+    margin-bottom: 2rem;
+}
+p {
+    font-family: Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif;
+    color: #666;
+}
+
+h5 {
+    font-weight: normal;
+    color: #999;
+    font-size: 1rem;
+}
+
+h6 {
+    font-weight: normal;
+    color: #999;
+}
+
+code {
+    font-weight: bold;
+    font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+}

+ 30 - 0
system/src/Grav/Common/Errors/Resources/layout.html.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Layout template file for Whoops's pretty error output.
+ */
+?>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>Whoops there was an error!</title>
+    <style><?php echo $stylesheet ?></style>
+</head>
+<body>
+    <div class="container">
+        <div class="details">
+            <header>
+                Server Error
+            </header>
+
+
+
+            <p>Sorry, something went terribly wrong!</p>
+
+            <h3><?php echo $code ?> - <?php echo $message ?></h3>
+
+            <h5>For further details please review your <code>logs/</code> folder, or enable displaying of errors in your system configuration.</h5>
+        </div>
+    </div>
+</body>
+</html>

+ 96 - 0
system/src/Grav/Common/Errors/SimplePageHandler.php

@@ -0,0 +1,96 @@
+<?php
+namespace Grav\Common\Errors;
+
+use Whoops\Handler\Handler;
+use Whoops\Util\Misc;
+use Whoops\Util\TemplateHelper;
+
+class SimplePageHandler extends Handler
+{
+    private $searchPaths = array();
+    private $resourceCache = array();
+
+    public function __construct()
+    {
+        // Add the default, local resource search path:
+        $this->searchPaths[] = __DIR__ . "/Resources";
+    }
+
+    /**
+     * @return int|null
+     */
+    public function handle()
+    {
+        $inspector = $this->getInspector();
+
+        $helper = new TemplateHelper();
+        $templateFile = $this->getResource("layout.html.php");
+        $cssFile      = $this->getResource("error.css");
+
+        $code = $inspector->getException()->getCode();
+        $message = $inspector->getException()->getMessage();
+
+        if ($inspector->getException() instanceof \ErrorException) {
+            $code = Misc::translateErrorCode($code);
+        }
+
+        $vars = array(
+            "stylesheet" => file_get_contents($cssFile),
+            "code"        => $code,
+            "message"     => $message,
+        );
+
+        $helper->setVariables($vars);
+        $helper->render($templateFile);
+
+        return Handler::QUIT;
+    }
+
+    /**
+     * @param $resource
+     *
+     * @return string
+     */
+    protected function getResource($resource)
+    {
+        // If the resource was found before, we can speed things up
+        // by caching its absolute, resolved path:
+        if (isset($this->resourceCache[$resource])) {
+            return $this->resourceCache[$resource];
+        }
+
+        // Search through available search paths, until we find the
+        // resource we're after:
+        foreach ($this->searchPaths as $path) {
+            $fullPath = $path . "/$resource";
+
+            if (is_file($fullPath)) {
+                // Cache the result:
+                $this->resourceCache[$resource] = $fullPath;
+                return $fullPath;
+            }
+        }
+
+        // If we got this far, nothing was found.
+        throw new \RuntimeException(
+            "Could not find resource '$resource' in any resource paths."
+            . "(searched: " . join(", ", $this->searchPaths). ")"
+        );
+    }
+
+    public function addResourcePath($path)
+    {
+        if (!is_dir($path)) {
+            throw new \InvalidArgumentException(
+                "'$path' is not a valid directory"
+            );
+        }
+
+        array_unshift($this->searchPaths, $path);
+    }
+
+    public function getResourcePaths()
+    {
+        return $this->searchPaths;
+    }
+}

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

@@ -0,0 +1,73 @@
+<?php
+namespace Grav\Common\File;
+
+use RocketTheme\Toolbox\File\PhpFile;
+
+/**
+ * Class CompiledFile
+ * @package Grav\Common\File
+ *
+ * @property string $filename
+ * @property string $extension
+ * @property string $raw
+ * @property array|string $content
+ */
+trait CompiledFile
+{
+    /**
+     * Get/set parsed file contents.
+     *
+     * @param mixed $var
+     * @return string
+     */
+    public function content($var = null)
+    {
+        // Set some options
+        $this->settings(['native' => true, 'compat' => true]);
+
+        // If nothing has been loaded, attempt to get pre-compiled version of the file first.
+        if ($var === null && $this->raw === null && $this->content === null) {
+            $key = md5($this->filename);
+            $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
+            $modified = $this->modified();
+
+            if (!$modified) {
+                return $this->decode($this->raw());
+            }
+
+            $class = get_class($this);
+
+            $cache = $file->exists() ? $file->content() : null;
+
+            // Load real file if cache isn't up to date (or is invalid).
+            if (
+                !isset($cache['@class'])
+                || $cache['@class'] != $class
+                || $cache['modified'] != $modified
+                || $cache['filename'] != $this->filename
+            ) {
+                // Attempt to lock the file for writing.
+                $file->lock(false);
+
+                // Decode RAW file into compiled array.
+                $data = (array) $this->decode($this->raw());
+                $cache = [
+                    '@class' => $class,
+                    'filename' => $this->filename,
+                    'modified' => $modified,
+                    'data' => $data
+                ];
+
+                // If compiled file wasn't already locked by another process, save it.
+                if ($file->locked() !== false) {
+                    $file->save($cache);
+                    $file->unlock();
+                }
+            }
+
+            $this->content = $cache['data'];
+        }
+
+        return parent::content($var);
+    }
+}

+ 9 - 0
system/src/Grav/Common/File/CompiledMarkdownFile.php

@@ -0,0 +1,9 @@
+<?php
+namespace Grav\Common\File;
+
+use RocketTheme\Toolbox\File\MarkdownFile;
+
+class CompiledMarkdownFile extends MarkdownFile
+{
+    use CompiledFile;
+}

+ 9 - 0
system/src/Grav/Common/File/CompiledYamlFile.php

@@ -0,0 +1,9 @@
+<?php
+namespace Grav\Common\File;
+
+use RocketTheme\Toolbox\File\YamlFile;
+
+class CompiledYamlFile extends YamlFile
+{
+    use CompiledFile;
+}

+ 353 - 0
system/src/Grav/Common/Filesystem/Folder.php

@@ -0,0 +1,353 @@
+<?php
+namespace Grav\Common\Filesystem;
+
+/**
+ * Folder helper class.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+abstract class Folder
+{
+    /**
+     * Recursively find the last modified time under given path.
+     *
+     * @param  string $path
+     * @return int
+     */
+    public static function lastModifiedFolder($path)
+    {
+        $last_modified = 0;
+
+        $dirItr     = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
+        $filterItr  = new RecursiveFolderFilterIterator($dirItr);
+        $itr        = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
+
+        /** @var \RecursiveDirectoryIterator $file */
+        foreach ($itr as $dir) {
+            $dir_modified = $dir->getMTime();
+            if ($dir_modified > $last_modified) {
+                $last_modified = $dir_modified;
+            }
+        }
+
+        return $last_modified;
+    }
+
+    /**
+     * Recursively find the last modified time under given path by file.
+     *
+     * @param  string $path
+     * @param string  $extensions   which files to search for specifically
+     *
+     * @return int
+     */
+    public static function lastModifiedFile($path, $extensions = 'md|yaml')
+    {
+        $last_modified = 0;
+
+        $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
+        $itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST);
+        $itr = new \RegexIterator($itrItr, '/^.+\.'.$extensions.'$/i');
+
+        /** @var \RecursiveDirectoryIterator $file */
+        foreach ($itr as $filepath => $file) {
+            $file_modified = $file->getMTime();
+            if ($file_modified > $last_modified) {
+                $last_modified = $file_modified;
+            }
+        }
+
+        return $last_modified;
+    }
+
+    /**
+     * Get relative path between target and base path. If path isn't relative, return full path.
+     *
+     * @param  string  $path
+     * @param  string  $base
+     * @return string
+     */
+    public static function getRelativePath($path, $base = GRAV_ROOT)
+    {
+        if ($base) {
+            $base = preg_replace('![\\\/]+!', '/', $base);
+            $path = preg_replace('![\\\/]+!', '/', $path);
+            if (strpos($path, $base) === 0) {
+                $path = ltrim(substr($path, strlen($base)), '/');
+            }
+        }
+
+        return $path;
+    }
+
+    /**
+     * Shift first directory out of the path.
+     *
+     * @param string $path
+     * @return string
+     */
+    public static function shift(&$path)
+    {
+        $parts = explode('/', trim($path, '/'), 2);
+        $result = array_shift($parts);
+        $path = array_shift($parts);
+
+        return $result ?: null;
+    }
+
+
+
+    /**
+     * Return recursive list of all files and directories under given path.
+     *
+     * @param  string            $path
+     * @param  array             $params
+     * @return array
+     * @throws \RuntimeException
+     */
+    public static function all($path, array $params = array())
+    {
+        if ($path === false) {
+            throw new \RuntimeException("Path to {$path} doesn't exist.");
+        }
+
+        $compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
+        $pattern = isset($params['pattern']) ? $params['pattern'] : null;
+        $filters = isset($params['filters']) ? $params['filters'] : null;
+        $recursive = isset($params['recursive']) ? $params['recursive'] : true;
+        $key = isset($params['key']) ? 'get' . $params['key'] : null;
+        $value = isset($params['value']) ? 'get' . $params['value'] : ($recursive ? 'getSubPathname' : 'getFilename');
+
+        if ($recursive) {
+            $directory = new \RecursiveDirectoryIterator($path,
+                \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF);
+            $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
+        } else {
+            $iterator = new \FilesystemIterator($path);
+        }
+
+        $results = array();
+
+        /** @var \RecursiveDirectoryIterator $file */
+        foreach ($iterator as $file) {
+            if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
+                continue;
+            }
+            $fileKey = $key ? $file->{$key}() : null;
+            $filePath = $file->{$value}();
+            if ($filters) {
+                if (isset($filters['key'])) {
+                    $fileKey = preg_replace($filters['key'], '', $fileKey);
+                }
+                if (isset($filters['value'])) {
+                    $filter = $filters['value'];
+                    if (is_callable($filter)) {
+                        $filePath = call_user_func($filter, $file);
+                    } else {
+                        $filePath = preg_replace($filter, '', $filePath);
+                }
+            }
+            }
+
+            if ($fileKey !== null) {
+            $results[$fileKey] = $filePath;
+            } else {
+                $results[] = $filePath;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Recursively copy directory in filesystem.
+     *
+     * @param  string            $source
+     * @param  string            $target
+     * @throws \RuntimeException
+     */
+    public static function copy($source, $target)
+    {
+        $source = rtrim($source, '\\/');
+        $target = rtrim($target, '\\/');
+
+        if (!is_dir($source)) {
+            throw new \RuntimeException('Cannot copy non-existing folder.');
+        }
+
+        // Make sure that path to the target exists before copying.
+        self::mkdir($target);
+
+        $success = true;
+
+        // Go through all sub-directories and copy everything.
+        $files = self::all($source);
+        foreach ($files as $file) {
+            $src = $source .'/'. $file;
+            $dst = $target .'/'. $file;
+
+            if (is_dir($src)) {
+                // Create current directory.
+                $success &= @mkdir($dst);
+            } else {
+                // Or copy current file.
+                $success &= @copy($src, $dst);
+            }
+        }
+
+        if (!$success) {
+            $error = error_get_last();
+            throw new \RuntimeException($error['message']);
+        }
+
+        // Make sure that the change will be detected when caching.
+        @touch(dirname($target));
+    }
+
+    /**
+     * Move directory in filesystem.
+     *
+     * @param  string            $source
+     * @param  string            $target
+     * @throws \RuntimeException
+     */
+    public static function move($source, $target)
+    {
+        if (!is_dir($source)) {
+            throw new \RuntimeException('Cannot move non-existing folder.');
+        }
+
+        // Make sure that path to the target exists before moving.
+        self::mkdir(dirname($target));
+
+        // Just rename the directory.
+        $success = @rename($source, $target);
+
+        if (!$success) {
+            $error = error_get_last();
+            throw new \RuntimeException($error['message']);
+        }
+
+        // Make sure that the change will be detected when caching.
+        @touch(dirname($source));
+        @touch(dirname($target));
+    }
+
+    /**
+     * Recursively delete directory from filesystem.
+     *
+     * @param  string $target
+     * @throws \RuntimeException
+     * @return bool
+     */
+    public static function delete($target)
+    {
+        if (!is_dir($target)) {
+            return;
+        }
+
+        $success = self::doDelete($target);
+
+        if (!$success) {
+            $error = error_get_last();
+            throw new \RuntimeException($error['message']);
+        }
+
+        // Make sure that the change will be detected when caching.
+        @touch(dirname($target));
+        return $success;
+    }
+
+    /**
+     * @param  string            $folder
+     * @throws \RuntimeException
+     * @internal
+     */
+    public static function mkdir($folder)
+    {
+        if (is_dir($folder)) {
+            return;
+        }
+
+        $success = @mkdir($folder, 0777, true);
+
+        if (!$success) {
+            $error = error_get_last();
+            throw new \RuntimeException($error['message']);
+        }
+    }
+
+    /**
+     * Recursive copy of one directory to another
+     *
+     * @param $src
+     * @param $dest
+     *
+     * @return bool
+     */
+    public static function rcopy($src, $dest)
+    {
+
+        // If the src is not a directory do a simple file copy
+        if (!is_dir($src)) {
+            copy($src, $dest);
+            return true;
+        }
+
+        // If the destination directory does not exist create it
+        if (!is_dir($dest)) {
+            if (!mkdir($dest)) {
+                // If the destination directory could not be created stop processing
+                return false;
+            }
+        }
+
+        // Open the source directory to read in files
+        $i = new \DirectoryIterator($src);
+        /** @var \DirectoryIterator $f */
+        foreach ($i as $f) {
+            if ($f->isFile()) {
+                copy($f->getRealPath(), "$dest/" . $f->getFilename());
+            } else {
+                if (!$f->isDot() && $f->isDir()) {
+                    static::rcopy($f->getRealPath(), "$dest/$f");
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @param  string $folder
+     * @return bool
+     * @internal
+     */
+    protected static function doDelete($folder)
+    {
+        // Special case for symbolic links.
+        if (is_link($folder)) {
+            return @unlink($folder);
+        }
+
+        $files = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS),
+            \RecursiveIteratorIterator::CHILD_FIRST
+        );
+
+        /** @var \DirectoryIterator $fileinfo */
+        foreach ($files as $fileinfo) {
+            if ($fileinfo->isDir()) {
+                if (false === rmdir($fileinfo->getRealPath())) {
+                    return false;
+                }
+            } else {
+                if (false === unlink($fileinfo->getRealPath())) {
+                    return false;
+                }
+            }
+        }
+
+        return rmdir($folder);
+    }
+}

+ 31 - 0
system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php

@@ -0,0 +1,31 @@
+<?php
+namespace Grav\Common\Filesystem;
+
+use Grav\Common\GravTrait;
+
+class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
+{
+    use GravTrait;
+
+    protected static $folder_ignores;
+
+    public function __construct(\RecursiveIterator $iterator)
+    {
+        parent::__construct($iterator);
+        if (empty($this::$folder_ignores)) {
+            $this::$folder_ignores = self::getGrav()['config']->get('system.pages.ignore_folders');
+        }
+    }
+
+    public function accept()
+    {
+
+        /** @var $current \SplFileInfo */
+        $current = $this->current();
+
+        if ($current->isDir() && !in_array($current->getFilename(), $this::$folder_ignores)) {
+            return true;
+        }
+        return false;
+    }
+}

+ 32 - 0
system/src/Grav/Common/GPM/AbstractCollection.php

@@ -0,0 +1,32 @@
+<?php
+namespace Grav\Common\GPM;
+
+use Grav\Common\GravTrait;
+use Grav\Common\Iterator;
+
+abstract class AbstractCollection extends Iterator {
+
+    use GravTrait;
+
+    public function toJson()
+    {
+        $items = [];
+
+        foreach ($this->items as $name => $package) {
+            $items[$name] = $package->toArray();
+        }
+
+        return json_encode($items);
+    }
+
+    public function toArray()
+    {
+        $items = [];
+
+        foreach ($this->items as $name => $package) {
+            $items[$name] = $package->toArray();
+        }
+
+        return $items;
+    }
+}

+ 34 - 0
system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php

@@ -0,0 +1,34 @@
+<?php
+namespace Grav\Common\GPM\Common;
+
+use Grav\Common\GravTrait;
+use Grav\Common\Iterator;
+
+abstract class AbstractPackageCollection extends Iterator {
+
+    use GravTrait;
+
+    protected $type;
+
+    public function toJson()
+    {
+        $items = [];
+
+        foreach ($this->items as $name => $package) {
+            $items[$name] = $package->toArray();
+        }
+
+        return json_encode($items);
+    }
+
+    public function toArray()
+    {
+        $items = [];
+
+        foreach ($this->items as $name => $package) {
+            $items[$name] = $package->toArray();
+        }
+
+        return $items;
+    }
+}

+ 21 - 0
system/src/Grav/Common/GPM/Common/CachedCollection.php

@@ -0,0 +1,21 @@
+<?php
+namespace Grav\Common\GPM\Common;
+
+use Grav\Common\Iterator;
+
+class CachedCollection extends Iterator {
+
+    protected static $cache;
+
+    public function __construct($items)
+    {
+        // local cache to speed things up
+        if (!isset(self::$cache[get_called_class().__METHOD__])) {
+            self::$cache[get_called_class().__METHOD__] = $items;
+        }
+
+        foreach (self::$cache[get_called_class().__METHOD__] as $name => $item) {
+            $this->append([$name => $item]);
+        }
+    }
+}

+ 42 - 0
system/src/Grav/Common/GPM/Common/Package.php

@@ -0,0 +1,42 @@
+<?php
+namespace Grav\Common\GPM\Common;
+
+use Grav\Common\Data\Data;
+
+class Package {
+
+    protected $data;
+
+    public function __construct(Data $package, $type = null) {
+        $this->data = $package;
+
+        if ($type) {
+            $this->data->set('package_type', $type);
+        }
+    }
+
+    public function getData() {
+        return $this->data;
+    }
+
+    public function __get($key) {
+        return $this->data->get($key);
+    }
+
+    public function __isset($key) {
+        return isset($this->data->$key);
+    }
+
+    public function __toString() {
+        return $this->toJson();
+    }
+
+    public function toJson() {
+        return $this->data->toJson();
+    }
+
+    public function toArray() {
+        return $this->data->toArray();
+    }
+
+}

+ 398 - 0
system/src/Grav/Common/GPM/GPM.php

@@ -0,0 +1,398 @@
+<?php
+namespace Grav\Common\GPM;
+
+use Grav\Common\Inflector;
+use Grav\Common\Iterator;
+use Grav\Common\Utils;
+
+class GPM extends Iterator
+{
+    /**
+     * Local installed Packages
+     * @var Local\Packages
+     */
+    private $installed;
+
+    /**
+     * Remote available Packages
+     * @var Remote\Packages
+     */
+    private $repository;
+
+    /**
+     * @var Remote\Grav
+     */
+    public $grav;
+
+    /**
+     * Internal cache
+     * @var
+     */
+    protected $cache;
+
+    protected $install_paths = ['plugins' => 'user/plugins/%name%', 'themes' => 'user/themes/%name%', 'skeletons' => 'user/'];
+
+    /**
+     * Creates a new GPM instance with Local and Remote packages available
+     * @param boolean  $refresh  Applies to Remote Packages only and forces a refetch of data
+     * @param callable $callback Either a function or callback in array notation
+     */
+    public function __construct($refresh = false, $callback = null)
+    {
+        $this->installed  = new Local\Packages();
+        try {
+            $this->repository = new Remote\Packages($refresh, $callback);
+            $this->grav       = new Remote\Grav($refresh, $callback);
+        } catch (\Exception $e) {
+        }
+    }
+
+    /**
+     * Returns the Locally installed packages
+     * @return Iterator The installed packages
+     */
+    public function getInstalled()
+    {
+        return $this->installed;
+    }
+
+    /**
+     * Returns the amount of locally installed packages
+     * @return integer Amount of installed packages
+     */
+    public function countInstalled()
+    {
+        $installed = $this->getInstalled();
+
+        return count($installed['plugins']) + count($installed['themes']);
+    }
+
+    /**
+     * Return the instance of a specific Plugin
+     * @param  string  $slug The slug of the Plugin
+     * @return Local\Package The instance of the Plugin
+     */
+    public function getInstalledPlugin($slug)
+    {
+        return $this->installed['plugins'][$slug];
+    }
+
+    /**
+     * Returns the Locally installed plugins
+     * @return Iterator The installed plugins
+     */
+    public function getInstalledPlugins()
+    {
+        return $this->installed['plugins'];
+    }
+
+    /**
+     * Checks if a Plugin is installed
+     * @param  string  $slug The slug of the Plugin
+     * @return boolean True if the Plugin has been installed. False otherwise
+     */
+    public function isPluginInstalled($slug)
+    {
+        return isset($this->installed['plugins'][$slug]);
+    }
+
+    /**
+     * Return the instance of a specific Theme
+     * @param  string  $slug The slug of the Theme
+     * @return Local\Package The instance of the Theme
+     */
+    public function getInstalledTheme($slug)
+    {
+        return $this->installed['themes'][$slug];
+    }
+
+    /**
+     * Returns the Locally installed themes
+     * @return Iterator The installed themes
+     */
+    public function getInstalledThemes()
+    {
+        return $this->installed['themes'];
+    }
+
+    /**
+     * Checks if a Theme is installed
+     * @param  string  $slug The slug of the Theme
+     * @return boolean True if the Theme has been installed. False otherwise
+     */
+    public function isThemeInstalled($slug)
+    {
+        return isset($this->installed['themes'][$slug]);
+    }
+
+    /**
+     * Returns the amount of updates available
+     * @return integer Amount of available updates
+     */
+    public function countUpdates()
+    {
+        $count = 0;
+
+        $count += count($this->getUpdatablePlugins());
+        $count += count($this->getUpdatableThemes());
+
+        return $count;
+    }
+
+    /**
+     * Returns an array of Plugins and Themes that can be updated.
+     * Plugins and Themes are extended with the `available` property that relies to the remote version
+     * @return array Array of updatable Plugins and Themes.
+     *               Format: ['total' => int, 'plugins' => array, 'themes' => array]
+     */
+    public function getUpdatable()
+    {
+        $plugins = $this->getUpdatablePlugins();
+        $themes  = $this->getUpdatableThemes();
+
+        $items = [
+            'total'   => count($plugins)+count($themes),
+            'plugins' => $plugins,
+            'themes'  => $themes
+        ];
+
+        return $items;
+    }
+
+    /**
+     * Returns an array of Plugins that can be updated.
+     * The Plugins are extended with the `available` property that relies to the remote version
+     * @return Iterator Array of updatable Plugins
+     */
+    public function getUpdatablePlugins()
+    {
+        $items      = [];
+        $repository = $this->repository['plugins'];
+
+        // local cache to speed things up
+        if (isset($this->cache[__METHOD__])) {
+            return $this->cache[__METHOD__];
+        }
+
+        foreach ($this->installed['plugins'] as $slug => $plugin) {
+            if (!isset($repository[$slug]) || $plugin->symlink) {
+                continue;
+            }
+
+            $local_version  = $plugin->version ? $plugin->version : 'Unknown';
+            $remote_version = $repository[$slug]->version;
+
+            if (version_compare($local_version, $remote_version) < 0) {
+                $repository[$slug]->available = $remote_version;
+                $repository[$slug]->version   = $local_version;
+                $items[$slug]                 = $repository[$slug];
+            }
+        }
+
+        $this->cache[__METHOD__] = $items;
+
+        return $items;
+    }
+
+    /**
+     * Check if a Plugin or Theme is updatable
+     * @param  string  $slug The slug of the package
+     * @return boolean True if updatable. False otherwise or if not found
+     */
+    public function isUpdatable($slug)
+    {
+        return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug);
+    }
+
+    /**
+     * Checks if a Plugin is updatable
+     * @param  string  $plugin The slug of the Plugin
+     * @return boolean True if the Plugin is updatable. False otherwise
+     */
+    public function isPluginUpdatable($plugin)
+    {
+        return array_key_exists($plugin, (array) $this->getUpdatablePlugins());
+    }
+
+    /**
+     * Returns an array of Themes that can be updated.
+     * The Themes are extended with the `available` property that relies to the remote version
+     * @return Iterator Array of updatable Themes
+     */
+    public function getUpdatableThemes()
+    {
+        $items      = [];
+        $repository = $this->repository['themes'];
+
+        // local cache to speed things up
+        if (isset($this->cache[__METHOD__])) {
+            return $this->cache[__METHOD__];
+        }
+
+        foreach ($this->installed['themes'] as $slug => $plugin) {
+            if (!isset($repository[$slug]) || $plugin->symlink) {
+                continue;
+            }
+
+            $local_version  = $plugin->version ? $plugin->version : 'Unknown';
+            $remote_version = $repository[$slug]->version;
+
+            if (version_compare($local_version, $remote_version) < 0) {
+                $repository[$slug]->available = $remote_version;
+                $repository[$slug]->version   = $local_version;
+                $items[$slug]                 = $repository[$slug];
+            }
+        }
+
+        $this->cache[__METHOD__] = $items;
+
+        return $items;
+    }
+
+    /**
+     * Checks if a Theme is Updatable
+     * @param  string  $theme The slug of the Theme
+     * @return boolean True if the Theme is updatable. False otherwise
+     */
+    public function isThemeUpdatable($theme)
+    {
+        return array_key_exists($theme, (array) $this->getUpdatableThemes());
+    }
+
+    /**
+     * Returns a Plugin from the repository
+     * @param  string $slug The slug of the Plugin
+     * @return mixed  Package if found, NULL if not
+     */
+    public function getRepositoryPlugin($slug)
+    {
+        return @$this->repository['plugins'][$slug];
+    }
+
+    /**
+     * Returns the list of Plugins available in the repository
+     * @return Iterator The Plugins remotely available
+     */
+    public function getRepositoryPlugins()
+    {
+        return $this->repository['plugins'];
+    }
+
+    /**
+     * Returns a Theme from the repository
+     * @param  string $slug The slug of the Theme
+     * @return mixed  Package if found, NULL if not
+     */
+    public function getRepositoryTheme($slug)
+    {
+        return @$this->repository['themes'][$slug];
+    }
+
+    /**
+     * Returns the list of Themes available in the repository
+     * @return Iterator The Themes remotely available
+     */
+    public function getRepositoryThemes()
+    {
+        return $this->repository['themes'];
+    }
+
+    /**
+     * Returns the list of Plugins and Themes available in the repository
+     * @return array Array of available Plugins and Themes
+     *               Format: ['plugins' => array, 'themes' => array]
+     */
+    public function getRepository()
+    {
+        return $this->repository;
+    }
+
+    /**
+     * Searches for a Package in the repository
+     * @param  string  $search Can be either the slug or the name
+     * @return Remote\Package Package if found, FALSE if not
+     */
+    public function findPackage($search)
+    {
+        $search = strtolower($search);
+        if ($found = $this->getRepositoryTheme($search)) {
+            return $found;
+        }
+
+        if ($found = $this->getRepositoryPlugin($search)) {
+            return $found;
+        }
+
+        foreach ($this->getRepositoryThemes() as $slug => $theme) {
+            if ($search == $slug || $search == $theme->name) {
+                return $theme;
+            }
+        }
+
+        foreach ($this->getRepositoryPlugins() as $slug => $plugin) {
+            if ($search == $slug || $search == $plugin->name) {
+                return $plugin;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the list of Plugins and Themes available in the repository
+     * @return array Array of available Plugins and Themes
+     *               Format: ['plugins' => array, 'themes' => array]
+     */
+    /**
+     * Searches for a list of Packages in the repository
+     * @param  array $searches An array of either slugs or names
+     * @return array Array of found Packages
+     *                        Format: ['total' => int, 'not_found' => array, <found-slugs>]
+     */
+    public function findPackages($searches = [])
+    {
+        $packages = ['total' => 0, 'not_found' => []];
+        $inflector = new Inflector();
+
+        foreach ($searches as $search) {
+            $repository = '';
+            // if this is an object, get the search data from the key
+            if (is_object($search)) {
+                $search = (array) $search;
+                $key = key($search);
+                $repository = $search[$key];
+                $search = $key;
+            }
+
+            if ($found = $this->findPackage($search)) {
+                // set override repository if provided
+                if ($repository) {
+                    $found->override_repository = $repository;
+                }
+                if (!isset($packages[$found->package_type])) {
+                    $packages[$found->package_type] = [];
+                }
+
+                $packages[$found->package_type][$found->slug] = $found;
+                $packages['total']++;
+            } else {
+                // make a best guess at the type based on the repo URL
+                if (Utils::contains($repository, '-theme')) {
+                    $type = 'themes';
+                } else {
+                    $type = 'plugins';
+                }
+
+                $not_found = new \stdClass();
+                $not_found->name = $inflector->camelize($search);
+                $not_found->slug = $search;
+                $not_found->package_type = $type;
+                $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]);
+                $not_found->override_repository = $repository;
+                $packages['not_found'][$search] = $not_found;
+            }
+        }
+
+        return $packages;
+    }
+}

+ 343 - 0
system/src/Grav/Common/GPM/Installer.php

@@ -0,0 +1,343 @@
+<?php
+namespace Grav\Common\GPM;
+
+use Grav\Common\Filesystem\Folder;
+use Symfony\Component\Yaml\Yaml;
+
+class Installer
+{
+    /** @const No error */
+    const OK = 0;
+    /** @const Target already exists */
+    const EXISTS = 1;
+    /** @const Target is a symbolic link */
+    const IS_LINK = 2;
+    /** @const Target doesn't exist */
+    const NOT_FOUND = 4;
+    /** @const Target is not a directory */
+    const NOT_DIRECTORY = 8;
+    /** @const Target is not a Grav instance */
+    const NOT_GRAV_ROOT = 16;
+    /** @const Error while trying to open the ZIP package */
+    const ZIP_OPEN_ERROR = 32;
+    /** @const Error while trying to extract the ZIP package */
+    const ZIP_EXTRACT_ERROR = 64;
+
+    /**
+     * Destination folder on which validation checks are applied
+     * @var string
+     */
+    protected static $target;
+
+    /**
+     * Error Code
+     * @var integer
+     */
+    protected static $error = 0;
+
+    /**
+     * Default options for the install
+     * @var array
+     */
+    protected static $options = [
+        'overwrite'       => true,
+        'ignore_symlinks' => true,
+        'sophisticated'   => false,
+        'theme'            => false,
+        'install_path'    => '',
+        'exclude_checks'  => [self::EXISTS, self::NOT_FOUND, self::IS_LINK]
+    ];
+
+    /**
+     * Installs a given package to a given destination.
+     *
+     * @param  string $package     The local path to the ZIP package
+     * @param  string $destination The local path to the Grav Instance
+     * @param  array  $options     Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
+     *
+     * @return boolean True if everything went fine, False otherwise.
+     */
+    public static function install($package, $destination, $options = [])
+    {
+        $destination = rtrim($destination, DS);
+        $options = array_merge(self::$options, $options);
+        $install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS);
+
+        if (!self::isGravInstance($destination) || !self::isValidDestination($install_path, $options['exclude_checks'])) {
+            return false;
+        }
+
+        if (self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] ||
+            self::lastErrorCode() == self::EXISTS && !$options['overwrite']) {
+            return false;
+        }
+
+        // Pre install checks
+        static::flightProcessing('pre_install', $install_path);
+
+        $zip = new \ZipArchive();
+        $archive = $zip->open($package);
+        $tmp = CACHE_DIR . 'tmp/Grav-' . uniqid();
+
+        if ($archive !== true) {
+            self::$error = self::ZIP_OPEN_ERROR;
+
+            return false;
+        }
+
+        Folder::mkdir($tmp);
+
+        $unzip = $zip->extractTo($tmp);
+
+        if (!$unzip) {
+            self::$error = self::ZIP_EXTRACT_ERROR;
+
+            $zip->close();
+            Folder::delete($tmp);
+
+            return false;
+        }
+
+
+        if (!$options['sophisticated']) {
+            if ($options['theme']) {
+                self::copyInstall($zip, $install_path, $tmp);
+            } else {
+                self::moveInstall($zip, $install_path, $tmp);
+            }
+        } else {
+            self::sophisticatedInstall($zip, $install_path, $tmp);
+        }
+
+        Folder::delete($tmp);
+        $zip->close();
+
+        // Post install checks
+        static::flightProcessing('post_install', $install_path);
+
+        self::$error = self::OK;
+
+        return true;
+
+    }
+
+    protected static function flightProcessing($state, $install_path)
+    {
+        $blueprints_path = $install_path . DS . 'blueprints.yaml';
+
+        if (file_exists($blueprints_path)) {
+            $package_yaml = Yaml::parse(file_get_contents($blueprints_path));
+            if (isset($package_yaml['install'][$state]['create'])) {
+                foreach ((array) $package_yaml['install']['pre_install']['create'] as $file) {
+                    Folder::mkdir($install_path . '/' . ltrim($file, '/'));
+                }
+            }
+            if (isset($package_yaml['install'][$state]['remove'])) {
+                foreach ((array) $package_yaml['install']['pre_install']['remove'] as $file) {
+                    Folder::delete($install_path . '/' . ltrim($file, '/'));
+                }
+            }
+        }
+    }
+
+    public static function moveInstall(\ZipArchive $zip, $install_path, $tmp)
+    {
+        $container = $zip->getNameIndex(0);
+        if (file_exists($install_path)) {
+            Folder::delete($install_path);
+        }
+
+        Folder::move($tmp . DS . $container, $install_path);
+
+        return true;
+    }
+
+    public static function copyInstall(\ZipArchive $zip, $install_path, $tmp)
+    {
+        $firstDir = $zip->getNameIndex(0);
+        if (empty($firstDir)) {
+            throw new \RuntimeException("Directory $firstDir is missing");
+        } else {
+            $tmp = realpath($tmp . DS . $firstDir);
+            Folder::rcopy($tmp, $install_path);
+        }
+
+        return true;
+    }
+
+    public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp)
+    {
+        for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) {
+            $filename = $zip->getNameIndex($i);
+            $fileinfo = pathinfo($filename);
+            $depth = count(explode(DS, rtrim($filename, '/')));
+
+            if ($depth > 2) {
+                continue;
+            }
+
+            $path = $install_path . DS . $fileinfo['basename'];
+
+            if (is_link($path)) {
+                continue;
+            } else {
+                if (is_dir($path)) {
+                    Folder::delete($path);
+                    Folder::move($tmp . DS . $filename, $path);
+
+                    if ($fileinfo['basename'] == 'bin') {
+                        foreach (glob($path . DS . '*') as $file) {
+                            @chmod($file, 0755);
+                        }
+                    }
+                } else {
+                    @unlink($path);
+                    @copy($tmp . DS . $filename, $path);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Uninstalls one or more given package
+     *
+     * @param  string $path     The slug of the package(s)
+     * @param  array  $options     Options to use for uninstalling
+     *
+     * @return boolean True if everything went fine, False otherwise.
+     */
+    public static function uninstall($path, $options = [])
+    {
+        $options = array_merge(self::$options, $options);
+        if (!self::isValidDestination($path, $options['exclude_checks'])
+        ) {
+            return false;
+        }
+
+        return Folder::delete($path);
+    }
+
+    /**
+     * Runs a set of checks on the destination and sets the Error if any
+     *
+     * @param  string $destination The directory to run validations at
+     * @param  array  $exclude     An array of constants to exclude from the validation
+     *
+     * @return boolean True if validation passed. False otherwise
+     */
+    public static function isValidDestination($destination, $exclude = [])
+    {
+        self::$error = 0;
+        self::$target = $destination;
+
+        if (is_link($destination)) {
+            self::$error = self::IS_LINK;
+        } elseif (file_exists($destination)) {
+            self::$error = self::EXISTS;
+        } elseif (!file_exists($destination)) {
+            self::$error = self::NOT_FOUND;
+        } elseif (!is_dir($destination)) {
+            self::$error = self::NOT_DIRECTORY;
+        }
+
+        if (count($exclude) && in_array(self::$error, $exclude)) {
+            return true;
+        }
+
+        return !(self::$error);
+    }
+
+    /**
+     * Validates if the given path is a Grav Instance
+     *
+     * @param  string $target The local path to the Grav Instance
+     *
+     * @return boolean True if is a Grav Instance. False otherwise
+     */
+    public static function isGravInstance($target)
+    {
+        self::$error = 0;
+        self::$target = $target;
+
+        if (
+            !file_exists($target . DS . 'index.php') ||
+            !file_exists($target . DS . 'bin') ||
+            !file_exists($target . DS . 'user') ||
+            !file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml')
+        ) {
+            self::$error = self::NOT_GRAV_ROOT;
+        }
+
+        return !self::$error;
+    }
+
+    /**
+     * Returns the last error occurred in a string message format
+     * @return string The message of the last error
+     */
+    public static function lastErrorMsg()
+    {
+        $msg = 'Unknown Error';
+
+        switch (self::$error) {
+            case 0:
+                $msg = 'No Error';
+                break;
+
+            case self::EXISTS:
+                $msg = 'The target path "' . self::$target . '" already exists';
+                break;
+
+            case self::IS_LINK:
+                $msg = 'The target path "' . self::$target . '" is a symbolic link';
+                break;
+
+            case self::NOT_FOUND:
+                $msg = 'The target path "' . self::$target . '" does not appear to exist';
+                break;
+
+            case self::NOT_DIRECTORY:
+                $msg = 'The target path "' . self::$target . '" does not appear to be a folder';
+                break;
+
+            case self::NOT_GRAV_ROOT:
+                $msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance';
+                break;
+
+            case self::ZIP_OPEN_ERROR:
+                $msg = 'Unable to open the package file';
+                break;
+
+            case self::ZIP_EXTRACT_ERROR:
+                $msg = 'An error occurred while extracting the package';
+                break;
+
+            default:
+                return 'Unknown error';
+                break;
+        }
+
+        return $msg;
+    }
+
+    /**
+     * Returns the last error code of the occurred error
+     * @return integer The code of the last error
+     */
+    public static function lastErrorCode()
+    {
+        return self::$error;
+    }
+
+    /**
+     * Allows to manually set an error
+     * @param $error the Error code
+     */
+
+    public static function setError($error)
+    {
+        self::$error = $error;
+    }
+}

+ 17 - 0
system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Grav\Common\GPM\Local;
+
+use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
+use Grav\Common\GPM\Local\Package;
+
+abstract class AbstractPackageCollection extends BaseCollection {
+
+    public function __construct($items)
+    {
+        foreach ($items as $name => $data) {
+            $data->set('slug', $name);
+            $this->items[$name] = new Package($data, $this->type);
+        }
+    }
+}

+ 32 - 0
system/src/Grav/Common/GPM/Local/Package.php

@@ -0,0 +1,32 @@
+<?php
+namespace Grav\Common\GPM\Local;
+
+use Grav\Common\Data\Data;
+use Grav\Common\GPM\Common\Package as BasePackage;
+
+class Package extends BasePackage
+{
+    protected $settings;
+
+    public function __construct(Data $package, $package_type = null)
+    {
+        $data = new Data($package->blueprints()->toArray());
+        parent::__construct($data, $package_type);
+
+        $this->settings = $package->toArray();
+
+        $html_description = \Parsedown::instance()->line($this->description);
+        $this->data->set('slug', $package->slug);
+        $this->data->set('description_html', $html_description);
+        $this->data->set('description_plain', strip_tags($html_description));
+        $this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->name));
+    }
+
+    /**
+     * @return mixed
+     */
+    public function isEnabled()
+    {
+        return $this->settings['enabled'];
+    }
+}

+ 17 - 0
system/src/Grav/Common/GPM/Local/Packages.php

@@ -0,0 +1,17 @@
+<?php
+namespace Grav\Common\GPM\Local;
+
+use Grav\Common\GPM\Common\CachedCollection;
+
+class Packages extends CachedCollection
+{
+    public function __construct()
+    {
+        $items = [
+            'plugins' => new Plugins(),
+            'themes' => new Themes()
+        ];
+
+        parent::__construct($items);
+    }
+}

+ 22 - 0
system/src/Grav/Common/GPM/Local/Plugins.php

@@ -0,0 +1,22 @@
+<?php
+namespace Grav\Common\GPM\Local;
+
+/**
+ * Class Plugins
+ * @package Grav\Common\GPM\Local
+ */
+class Plugins extends AbstractPackageCollection
+{
+    /**
+     * @var string
+     */
+    protected $type = 'plugins';
+
+    /**
+     * Local Plugins Constructor
+     */
+    public function __construct()
+    {
+        parent::__construct(self::getGrav()['plugins']->all());
+    }
+}

+ 22 - 0
system/src/Grav/Common/GPM/Local/Themes.php

@@ -0,0 +1,22 @@
+<?php
+namespace Grav\Common\GPM\Local;
+
+/**
+ * Class Themes
+ * @package Grav\Common\GPM\Local
+ */
+class Themes extends AbstractPackageCollection
+{
+    /**
+     * @var string
+     */
+    protected $type = 'themes';
+
+    /**
+     * Local Themes Constructor
+     */
+    public function __construct()
+    {
+        parent::__construct(self::getGrav()['themes']->all());
+    }
+}

+ 58 - 0
system/src/Grav/Common/GPM/PackageInterface.php

@@ -0,0 +1,58 @@
+<?php
+namespace Grav\Common\GPM;
+
+use Grav\Common\Data\Data;
+
+/**
+ * Interface Package
+ * @package Grav\Common\GPM
+ */
+class Package
+{
+    /**
+     * @var Data
+     */
+    protected $data;
+
+    /**
+     * @var \Grav\Common\Data\Blueprint
+     */
+    protected $blueprints;
+
+    /**
+     * @param Data $package
+     * @param bool $package_type
+     */
+    public function __construct(Data $package, $package_type = false);
+
+    /**
+     * @return mixed
+     */
+    public function isEnabled();
+
+    /**
+     * @return Data
+     */
+    public function getData();
+
+    /**
+     * @param $key
+     * @return mixed
+     */
+    public function __get($key);
+
+    /**
+     * @return string
+     */
+    public function __toString();
+
+    /**
+     * @return string
+     */
+    public function toJson();
+
+    /**
+     * @return array
+     */
+    public function toArray();
+}

+ 55 - 0
system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php

@@ -0,0 +1,55 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
+use Grav\Common\GPM\Response;
+
+use \Doctrine\Common\Cache\FilesystemCache;
+
+class AbstractPackageCollection extends BaseCollection
+{
+    /**
+     * The cached data previously fetched
+     * @var string
+     */
+    protected $raw;
+
+    /**
+     * The lifetime to store the entry in seconds
+     * @var integer
+     */
+    private $lifetime = 86400;
+
+    protected $repository;
+
+    protected $cache;
+
+    public function __construct($repository = null, $refresh = false, $callback = null)
+    {
+        if ($repository === null) {
+            throw new \RuntimeException("A repository is required to indicate the origin of the remote collection");
+        }
+
+        $cache_dir = self::getGrav()['locator']->findResource('cache://gpm', true, true);
+        $this->cache = new FilesystemCache($cache_dir);
+
+        $this->repository = $repository;
+        $this->raw        = $this->cache->fetch(md5($this->repository));
+
+        $this->fetch($refresh, $callback);
+        foreach (json_decode($this->raw, true) as $slug => $data) {
+            $this->items[$slug] = new Package($data, $this->type);
+        }
+    }
+
+    public function fetch($refresh = false, $callback = null)
+    {
+        if (!$this->raw || $refresh) {
+            $response  = Response::get($this->repository, [], $callback);
+            $this->raw = $response;
+            $this->cache->save(md5($this->repository), $this->raw, $this->lifetime);
+        }
+
+        return $this->raw;
+    }
+}

+ 95 - 0
system/src/Grav/Common/GPM/Remote/Grav.php

@@ -0,0 +1,95 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+use \Doctrine\Common\Cache\FilesystemCache;
+
+class Grav extends AbstractPackageCollection
+{
+    protected $repository = 'http://getgrav.org/downloads/grav.json';
+    private $data;
+
+    private $version;
+    private $date;
+
+    /**
+     * @param bool $refresh
+     * @param null $callback
+     */
+    public function __construct($refresh = false, $callback = null)
+    {
+        $cache_dir      = self::getGrav()['locator']->findResource('cache://gpm', true, true);
+        $this->cache    = new FilesystemCache($cache_dir);
+        $this->raw      = $this->cache->fetch(md5($this->repository));
+
+        $this->fetch($refresh, $callback);
+
+        $this->data = json_decode($this->raw, true);
+        $this->version = isset($this->data['version']) ? $this->data['version'] : '-';
+        $this->date = isset($this->data['date']) ? $this->data['date'] : '-';
+
+        if (isset($this->data['assets'])) foreach ($this->data['assets'] as $slug => $data) {
+            $this->items[$slug] = new Package($data);
+        }
+    }
+
+    /**
+     * Returns the list of assets associated to the latest version of Grav
+     * @return array list of assets
+     */
+    public function getAssets()
+    {
+        return $this->data['assets'];
+    }
+
+    /**
+     * Returns the changelog list for each version of Grav
+     * @param string $diff the version number to start the diff from
+     *
+     * @return array changelog list for each version
+     */
+    public function getChangelog($diff = null)
+    {
+        if (!$diff) {
+            return $this->data['changelog'];
+        }
+
+        $diffLog = [];
+        foreach ($this->data['changelog'] as $version => $changelog) {
+            preg_match("/[\d\.]+/", $version, $cleanVersion);
+
+            if (!$cleanVersion || version_compare($diff, $cleanVersion[0], ">=")) { continue; }
+
+            $diffLog[$version] = $changelog;
+        }
+
+        return $diffLog;
+    }
+
+    /**
+     * Returns the latest version of Grav available remotely
+     * @return string
+     */
+    public function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * Return the release date of the latest Grav
+     * @return string
+     */
+    public function getDate()
+    {
+        return $this->date;
+    }
+
+    public function isUpdatable()
+    {
+        return version_compare(GRAV_VERSION, $this->getVersion(), '<');
+    }
+
+    public function isSymlink()
+    {
+        return is_link(GRAV_ROOT . DS . 'index.php');
+    }
+}

+ 12 - 0
system/src/Grav/Common/GPM/Remote/Package.php

@@ -0,0 +1,12 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+use Grav\Common\Data\Data;
+use Grav\Common\GPM\Common\Package as BasePackage;
+
+class Package extends BasePackage {
+    public function __construct($package, $package_type = null) {
+        $data = new Data($package);
+        parent::__construct($data, $package_type);
+    }
+}

+ 17 - 0
system/src/Grav/Common/GPM/Remote/Packages.php

@@ -0,0 +1,17 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+use Grav\Common\GPM\Common\CachedCollection;
+
+class Packages extends CachedCollection
+{
+    public function __construct($refresh = false, $callback = null)
+    {
+        $items = [
+            'plugins' => new Plugins($refresh, $callback),
+            'themes' => new Themes($refresh, $callback)
+        ];
+
+        parent::__construct($items);
+    }
+}

+ 24 - 0
system/src/Grav/Common/GPM/Remote/Plugins.php

@@ -0,0 +1,24 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+/**
+ * Class Plugins
+ * @package Grav\Common\GPM\Remote
+ */
+class Plugins extends AbstractPackageCollection
+{
+    /**
+     * @var string
+     */
+    protected $type = 'plugins';
+
+    protected $repository = 'http://getgrav.org/downloads/plugins.json';
+
+    /**
+     * Local Plugins Constructor
+     */
+    public function __construct($refresh = false, $callback = null)
+    {
+        parent::__construct($this->repository, $refresh, $callback);
+    }
+}

+ 24 - 0
system/src/Grav/Common/GPM/Remote/Themes.php

@@ -0,0 +1,24 @@
+<?php
+namespace Grav\Common\GPM\Remote;
+
+/**
+ * Class Themes
+ * @package Grav\Common\GPM\Remote
+ */
+class Themes extends AbstractPackageCollection
+{
+    /**
+     * @var string
+     */
+    protected $type = 'themes';
+
+    protected $repository = 'http://getgrav.org/downloads/themes.json';
+
+    /**
+     * Local Themes Constructor
+     */
+    public function __construct($refresh = false, $callback = null)
+    {
+        parent::__construct($this->repository, $refresh, $callback);
+    }
+}

+ 221 - 0
system/src/Grav/Common/GPM/Response.php

@@ -0,0 +1,221 @@
+<?php
+namespace Grav\Common\GPM;
+
+class Response
+{
+    /**
+     * The callback for the progress
+     * @var callable    Either a function or callback in array notation
+     */
+    public static $callback = null;
+
+     /**
+     * Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method
+     * @var string
+     */
+    private static $method = 'auto';
+
+    /**
+     * Default parameters for `curl` and `fopen`
+     * @var array
+     */
+    private static $defaults = [
+
+        'curl' => [
+            CURLOPT_REFERER        => 'Grav GPM',
+            CURLOPT_USERAGENT      => 'Grav GPM',
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_FOLLOWLOCATION => true,
+            CURLOPT_TIMEOUT        => 15,
+            CURLOPT_HEADER         => false,
+            /**
+             * Example of callback parameters from within your own class
+             */
+            //CURLOPT_NOPROGRESS     => false,
+            //CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
+        ],
+        'fopen' => [
+            'method'          => 'GET',
+            'user_agent'      => 'Grav GPM',
+            'max_redirects'   => 5,
+            'follow_location' => 1,
+            'timeout'         => 15,
+            /**
+             * Example of callback parameters from within your own class
+             */
+            //'notification' => [$this, 'progress']
+        ]
+    ];
+
+    /**
+     * Sets the preferred method to use for making HTTP calls.
+     * @param string $method Default is `auto`
+     */
+    public static function setMethod($method = 'auto')
+    {
+        if (!in_array($method, ['auto', 'curl', 'fopen'])) {
+            $method = 'auto';
+        }
+
+        self::$method = $method;
+
+        return new self();
+    }
+
+    /**
+     * Makes a request to the URL by using the preferred method
+     * @param  string $uri     URL to call
+     * @param  array  $options An array of parameters for both `curl` and `fopen`
+     * @return string The response of the request
+     */
+    public static function get($uri = '', $options = [], $callback = null)
+    {
+        if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
+            throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
+        }
+
+        $options = array_replace_recursive(self::$defaults, $options);
+        $method  = 'get' . ucfirst(strtolower(self::$method));
+
+        self::$callback = $callback;
+        return static::$method($uri, $options, $callback);
+    }
+
+    /**
+     * Progress normalized for cURL and Fopen
+     * @param  args   Variable length of arguments passed in by stream method
+     * @return array Normalized array with useful data.
+     *               Format: ['code' => int|false, 'filesize' => bytes, 'transferred' => bytes, 'percent' => int]
+     */
+    public static function progress()
+    {
+        static $filesize = null;
+
+        $args           = func_get_args();
+        $isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) == 'curl';
+
+        $notification_code = !$isCurlResource ? $args[0] : false;
+        $bytes_transferred = $isCurlResource ? $args[2] : $args[4];
+
+        if ($isCurlResource) {
+            $filesize = $args[1];
+        } elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
+            $filesize = $args[5];
+        }
+
+        if ($bytes_transferred > 0) {
+            if ($notification_code == STREAM_NOTIFY_PROGRESS|STREAM_NOTIFY_COMPLETED || $isCurlResource) {
+
+                $progress = [
+                    'code'        => $notification_code,
+                    'filesize'    => $filesize,
+                    'transferred' => $bytes_transferred,
+                    'percent'     => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1)
+                ];
+
+                if (self::$callback !== null) {
+                    call_user_func_array(self::$callback, [$progress]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if cURL is available
+     * @return boolean
+     */
+    public static function isCurlAvailable()
+    {
+        return function_exists('curl_version');
+    }
+
+    /**
+     * Checks if the remote fopen request is enabled in PHP
+     * @return boolean
+     */
+    public static function isFopenAvailable()
+    {
+        return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
+    }
+
+    /**
+     * Automatically picks the preferred method
+     * @return string The response of the request
+     */
+    private static function getAuto()
+    {
+        if (self::isFopenAvailable()) {
+            return self::getFopen(func_get_args());
+        }
+
+        if (self::isCurlAvailable()) {
+            return self::getCurl(func_get_args());
+        }
+    }
+
+    /**
+     * Starts a HTTP request via cURL
+     * @return string The response of the request
+     */
+    private static function getCurl()
+    {
+        $args     = func_get_args();
+        $args     = count($args) > 1 ? $args : array_shift($args);
+
+        $uri      = $args[0];
+        $options  = $args[1];
+        $callback = $args[2];
+
+        $ch = curl_init($uri);
+        curl_setopt_array($ch, $options['curl']);
+
+        if ($callback) {
+            curl_setopt_array(
+                $ch,
+                [
+                    CURLOPT_NOPROGRESS       => false,
+                    CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
+                ]
+            );
+        }
+
+        $response = curl_exec($ch);
+
+        if ($errno = curl_errno($ch)) {
+            $error_message = curl_strerror($errno);
+            throw new \RuntimeException("cURL error ({$errno}):\n {$error_message}");
+        }
+
+        curl_close($ch);
+
+        return $response;
+    }
+
+    /**
+     * Starts a HTTP request via fopen
+     * @return string The response of the request
+     */
+    private static function getFopen()
+    {
+        if (count($args = func_get_args()) == 1) {
+            $args = $args[0];
+        }
+
+        $uri      = $args[0];
+        $options  = $args[1];
+        $callback = $args[2];
+
+        if ($callback) {
+            $options['fopen']['notification'] = ['self', 'progress'];
+        }
+
+        $stream  = stream_context_create(['http' => $options['fopen']], $options['fopen']);
+        $content = @file_get_contents($uri, false, $stream);
+
+        if ($content === false) {
+            throw new \RuntimeException("Error while trying to download '$uri'");
+        }
+
+        return $content;
+    }
+}

+ 93 - 0
system/src/Grav/Common/GPM/Upgrader.php

@@ -0,0 +1,93 @@
+<?php
+namespace Grav\Common\GPM;
+
+class Upgrader
+{
+    /**
+     * Remote details about latest Grav version
+     * @var Packages
+     */
+    private $remote;
+
+    /**
+     * Internal cache
+     * @var Iterator
+     */
+    protected $cache;
+
+    /**
+     * Creates a new GPM instance with Local and Remote packages available
+     * @param boolean  $refresh  Applies to Remote Packages only and forces a refetch of data
+     * @param callable $callback Either a function or callback in array notation
+     */
+    public function __construct($refresh = false, $callback = null)
+    {
+        $this->remote = new Remote\Grav($refresh, $callback);
+    }
+
+    /**
+     * Returns the release date of the latest version of Grav
+     * @return string
+     */
+    public function getReleaseDate()
+    {
+        return $this->remote->getDate();
+    }
+
+    /**
+     * Returns the version of the installed Grav
+     * @return string
+     */
+    public function getLocalVersion()
+    {
+        return GRAV_VERSION;
+    }
+
+    /**
+     * Returns the version of the remotely available Grav
+     * @return string
+     */
+    public function getRemoteVersion()
+    {
+        return $this->remote->getVersion();
+    }
+
+    /**
+     * Returns an array of assets available to download remotely
+     * @return array
+     */
+    public function getAssets()
+    {
+        return $this->remote->getAssets();
+    }
+
+    /**
+     * Returns the changelog list for each version of Grav
+     * @param string $diff the version number to start the diff from
+     *
+     * @return array return the changelog list for each version
+     */
+    public function getChangelog($diff = null)
+    {
+        return $this->remote->getChangelog($diff);
+    }
+
+    /**
+     * Checks if the currently installed Grav is upgradable to a newer version
+     * @return boolean True if it's upgradable, False otherwise.
+     */
+    public function isUpgradable()
+    {
+        return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<");
+    }
+
+    /**
+     * Checks if Grav is currently symbolically linked
+     * @return boolean True if Grav is symlinked, False otherwise.
+     */
+
+    public function isSymlink()
+    {
+        return $this->remote->isSymlink();
+    }
+}

+ 150 - 0
system/src/Grav/Common/Getters.php

@@ -0,0 +1,150 @@
+<?php
+namespace Grav\Common;
+
+/**
+ * Abstract class to implement magic __get(), __set(), __isset() and __unset().
+ * Also implements ArrayAccess.
+ *
+ * @author RocketTheme
+ * @license MIT
+ */
+abstract class Getters implements \ArrayAccess, \Countable
+{
+    /**
+     * Define variable used in getters.
+     *
+     * @var string
+     */
+    protected $gettersVariable = null;
+
+    /**
+     * Magic setter method
+     *
+     * @param mixed $offset Medium name value
+     * @param mixed $value  Medium value
+     */
+    public function __set($offset, $value)
+    {
+        $this->offsetSet($offset, $value);
+    }
+
+    /**
+     * Magic getter method
+     *
+     * @param  mixed $offset Medium name value
+     * @return mixed         Medium value
+     */
+    public function __get($offset)
+    {
+        return $this->offsetGet($offset);
+    }
+
+    /**
+     * Magic method to determine if the attribute is set
+     *
+     * @param  mixed   $offset Medium name value
+     * @return boolean         True if the value is set
+     */
+    public function __isset($offset)
+    {
+        return $this->offsetExists($offset);
+    }
+
+    /**
+     * Magic method to unset the attribute
+     *
+     * @param mixed $offset The name value to unset
+     */
+    public function __unset($offset)
+    {
+        $this->offsetUnset($offset);
+    }
+
+    /**
+     * @param mixed $offset
+     * @return bool
+     */
+    public function offsetExists($offset)
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            return isset($this->{$var}[$offset]);
+        } else {
+            return isset($this->{$offset});
+        }
+    }
+
+    /**
+     * @param mixed $offset
+     * @return mixed
+     */
+    public function offsetGet($offset)
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            return isset($this->{$var}[$offset]) ? $this->{$var}[$offset] : null;
+        } else {
+            return isset($this->{$offset}) ? $this->{$offset} : null;
+        }
+    }
+
+    /**
+     * @param mixed $offset
+     * @param mixed $value
+     */
+    public function offsetSet($offset, $value)
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            $this->{$var}[$offset] = $value;
+        } else {
+            $this->{$offset} = $value;
+        }
+    }
+
+    /**
+     * @param mixed $offset
+     */
+    public function offsetUnset($offset)
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            unset($this->{$var}[$offset]);
+        } else {
+            unset($this->{$offset});
+        }
+    }
+
+    /**
+     * @return int
+     */
+    public function count()
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            count($this->{$var});
+        } else {
+            count($this->toArray());
+        }
+    }
+
+    /**
+     * Returns an associative array of object properties.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if ($this->gettersVariable) {
+            $var = $this->gettersVariable;
+            return $this->{$var};
+        } else {
+            $properties = (array) $this;
+            $list = array();
+            foreach ($properties as $property => $value) {
+                if ($property[0] != "\0") $list[$property] = $value;
+            }
+            return $list;
+        }
+    }
+}

+ 510 - 0
system/src/Grav/Common/Grav.php

@@ -0,0 +1,510 @@
+<?php
+namespace Grav\Common;
+
+use Grav\Common\Language\Language;
+use Grav\Common\Page\Medium\ImageMedium;
+use Grav\Common\Page\Pages;
+use Grav\Common\Service\ConfigServiceProvider;
+use Grav\Common\Service\ErrorServiceProvider;
+use Grav\Common\Service\LoggerServiceProvider;
+use Grav\Common\Service\StreamsServiceProvider;
+use Grav\Common\Twig\Twig;
+use RocketTheme\Toolbox\DI\Container;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Event\EventDispatcher;
+
+/**
+ * Grav
+ *
+ * @author Andy Miller
+ * @link http://www.rockettheme.com
+ * @license http://opensource.org/licenses/MIT
+ *
+ * Influenced by Pico, Stacey, Kirby, PieCrust and other great platforms...
+ */
+class Grav extends Container
+{
+    /**
+     * @var string
+     */
+    public $output;
+
+    /**
+     * @var static
+     */
+    protected static $instance;
+
+    public static function instance(array $values = array())
+    {
+        if (!self::$instance) {
+            self::$instance = static::load($values);
+
+            GravTrait::setGrav(self::$instance);
+
+        } elseif ($values) {
+            $instance = self::$instance;
+            foreach ($values as $key => $value) {
+                $instance->offsetSet($key, $value);
+            }
+        }
+
+        return self::$instance;
+    }
+
+    protected static function load(array $values)
+    {
+        $container = new static($values);
+
+        $container['grav'] = $container;
+
+
+
+        $container['debugger'] = new Debugger();
+        $container['debugger']->startTimer('_init', 'Initialize');
+
+        $container->register(new LoggerServiceProvider);
+
+        $container->register(new ErrorServiceProvider);
+
+        $container['uri'] = function ($c) {
+            return new Uri($c);
+        };
+
+        $container['task'] = function ($c) {
+            return !empty($_POST['task']) ? $_POST['task'] : $c['uri']->param('task');
+        };
+
+        $container['events'] = function ($c) {
+            return new EventDispatcher;
+        };
+        $container['cache'] = function ($c) {
+            return new Cache($c);
+        };
+        $container['session'] = function ($c) {
+            return new Session($c);
+        };
+        $container['plugins'] = function ($c) {
+            return new Plugins();
+        };
+        $container['themes'] = function ($c) {
+            return new Themes($c);
+        };
+        $container['twig'] = function ($c) {
+            return new Twig($c);
+        };
+        $container['taxonomy'] = function ($c) {
+            return new Taxonomy($c);
+        };
+        $container['language'] = function ($c) {
+            return new Language($c);
+        };
+
+        $container['pages'] = function ($c) {
+            return new Page\Pages($c);
+        };
+
+        $container['assets'] = new Assets();
+
+        $container['page'] = function ($c) {
+            /** @var Pages $pages */
+            $pages = $c['pages'];
+            /** @var Language $language */
+            $language = $c['language'];
+
+            /** @var Uri $uri */
+            $uri = $c['uri'];
+
+            $path = rtrim($uri->path(), '/');
+            $path = $path ?: '/';
+
+            $page = $pages->dispatch($path);
+
+            // Redirection tests
+            if ($page) {
+                // Language-specific redirection scenarios
+                if ($language->enabled()) {
+                    if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) {
+                        $c->redirect($page->route());
+                    }
+                    if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) {
+                        $c->redirectLangSafe($page->route());
+                    }
+                }
+                // Default route test and redirect
+                if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) {
+                    $c->redirectLangSafe($page->route());
+                }
+            }
+
+            // if page is not found, try some fallback stuff
+            if (!$page || !$page->routable()) {
+
+                // Try fallback URL stuff...
+                $c->fallbackUrl($page, $path);
+
+                // If no page found, fire event
+                $event = $c->fireEvent('onPageNotFound');
+
+                if (isset($event->page)) {
+                    $page = $event->page;
+                } else {
+                    throw new \RuntimeException('Page Not Found', 404);
+                }
+            }
+            return $page;
+        };
+        $container['output'] = function ($c) {
+            return $c['twig']->processSite($c['uri']->extension());
+        };
+        $container['browser'] = function ($c) {
+            return new Browser();
+        };
+
+        $container['base_url_absolute'] = function ($c) {
+            return $c['config']->get('system.base_url_absolute') ?: $c['uri']->rootUrl(true);
+        };
+        $container['base_url_relative'] = function ($c) {
+            return $c['config']->get('system.base_url_relative') ?: $c['uri']->rootUrl(false);
+        };
+        $container['base_url'] = function ($c) {
+            return $c['config']->get('system.absolute_urls') ? $c['base_url_absolute'] : $c['base_url_relative'];
+        };
+
+        $container->register(new StreamsServiceProvider);
+        $container->register(new ConfigServiceProvider);
+
+        $container['inflector'] = new Inflector();
+
+        $container['debugger']->stopTimer('_init');
+
+        return $container;
+    }
+
+    public function process()
+    {
+        /** @var Debugger $debugger */
+        $debugger = $this['debugger'];
+
+
+
+        // Initialize configuration.
+        $debugger->startTimer('_config', 'Configuration');
+        $this['config']->init();
+        $this['errors']->resetHandlers();
+        $this['uri']->init();
+        $this['session']->init();
+
+        $debugger->init();
+        $this['config']->debug();
+        $debugger->stopTimer('_config');
+
+        // Use output buffering to prevent headers from being sent too early.
+        ob_start();
+        if ($this['config']->get('system.cache.gzip')) {
+            ob_start('ob_gzhandler');
+        }
+
+        // Initialize the timezone
+        if ($this['config']->get('system.timezone')) {
+            date_default_timezone_set($this['config']->get('system.timezone'));
+        }
+
+        // Initialize Locale if set and configured
+        if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
+            setlocale(LC_ALL, $this['language']->getLanguage());
+        } elseif ($this['config']->get('system.default_locale')) {
+            setlocale(LC_ALL, $this['config']->get('system.default_locale'));
+        }
+
+        $debugger->startTimer('streams', 'Streams');
+        $this['streams'];
+        $debugger->stopTimer('streams');
+
+        $debugger->startTimer('plugins', 'Plugins');
+        $this['plugins']->init();
+        $this->fireEvent('onPluginsInitialized');
+        $debugger->stopTimer('plugins');
+
+        $debugger->startTimer('themes', 'Themes');
+        $this['themes']->init();
+        $this->fireEvent('onThemeInitialized');
+        $debugger->stopTimer('themes');
+
+        $task = $this['task'];
+        if ($task) {
+            $this->fireEvent('onTask.' . $task);
+        }
+
+        $this['assets']->init();
+        $this->fireEvent('onAssetsInitialized');
+
+        $debugger->startTimer('twig', 'Twig');
+        $this['twig']->init();
+        $debugger->stopTimer('twig');
+
+        $debugger->startTimer('pages', 'Pages');
+        $this['pages']->init();
+        $this->fireEvent('onPagesInitialized');
+        $debugger->stopTimer('pages');
+        $this->fireEvent('onPageInitialized');
+
+        $debugger->addAssets();
+
+        // Process whole page as required
+        $debugger->startTimer('render', 'Render');
+        $this->output = $this['output'];
+        $this->fireEvent('onOutputGenerated');
+        $debugger->stopTimer('render');
+
+        // Set the header type
+        $this->header();
+        echo $this->output;
+        $debugger->render();
+
+        $this->fireEvent('onOutputRendered');
+
+        register_shutdown_function([$this, 'shutdown']);
+    }
+
+    /**
+     * Redirect browser to another location.
+     *
+     * @param string $route Internal route.
+     * @param int $code Redirection code (30x)
+     */
+    public function redirect($route, $code = null)
+    {
+        /** @var Uri $uri */
+        $uri = $this['uri'];
+
+        //Check for code in route
+        $regex = '/.*(\[(30[1-7])\])$/';
+        preg_match($regex, $route, $matches);
+        if ($matches) {
+            $route = str_replace($matches[1], '', $matches[0]);
+            $code = $matches[2];
+        }
+
+        if ($code == null) {
+            $code = $this['config']->get('system.pages.redirect_default_code', 301);
+        }
+
+        if (isset($this['session'])) {
+            $this['session']->close();
+        }
+
+        if ($uri->isExternal($route)) {
+            $url = $route;
+        } else {
+            $url = rtrim($uri->rootUrl(), '/') .'/'. trim($route, '/');
+        }
+
+        header("Location: {$url}", true, $code);
+        exit();
+    }
+
+    /**
+     * Redirect browser to another location taking language into account (preferred)
+     *
+     * @param string $route Internal route.
+     * @param int $code Redirection code (30x)
+     */
+    public function redirectLangSafe($route, $code = null)
+    {
+        /** @var Language $language */
+        $language = $this['language'];
+
+        if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) {
+            return $this->redirect($language->getLanguage() . $route, $code);
+        } else {
+            return $this->redirect($route, $code);
+        }
+    }
+
+    /**
+     * Returns mime type for the file format.
+     *
+     * @param string $format
+     * @return string
+     */
+    public function mime($format)
+    {
+        switch ($format) {
+            case 'json':
+                return 'application/json';
+            case 'html':
+                return 'text/html';
+            case 'atom':
+                return 'application/atom+xml';
+            case 'rss':
+                return 'application/rss+xml';
+            case 'xml':
+                return 'application/xml';
+        }
+        return 'text/html';
+    }
+
+    /**
+     * Set response header.
+     */
+    public function header()
+    {
+        $extension = $this['uri']->extension();
+
+        /** @var Page $page */
+        $page = $this['page'];
+
+        header('Content-type: ' . $this->mime($extension));
+
+        // Calculate Expires Headers if set to > 0
+        $expires = $page->expires();
+
+        if ($expires > 0) {
+            $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
+            header('Cache-Control: max-age=' . $expires);
+            header('Expires: '. $expires_date);
+        }
+
+        // Set the last modified time
+        if ($page->lastModified()) {
+            $last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
+            header('Last-Modified: ' . $last_modified_date);
+        }
+
+        // Calculate a Hash based on the raw file
+        if ($page->eTag()) {
+            header('ETag: ' . md5($page->raw() . $page->modified()));
+        }
+
+        // Set debugger data in headers
+        if (!($extension === null || $extension == 'html')) {
+            $this['debugger']->enabled(false);
+        }
+
+        // Set HTTP response code
+        if (isset($this['page']->header()->http_response_code)) {
+            http_response_code($this['page']->header()->http_response_code);
+        }
+
+        // Vary: Accept-Encoding
+        if ($this['config']->get('system.pages.vary_accept_encoding', false)) {
+            header('Vary: Accept-Encoding');
+        }
+    }
+
+    /**
+     * Fires an event with optional parameters.
+     *
+     * @param  string $eventName
+     * @param  Event  $event
+     * @return Event
+     */
+    public function fireEvent($eventName, Event $event = null)
+    {
+        /** @var EventDispatcher $events */
+        $events = $this['events'];
+        return $events->dispatch($eventName, $event);
+    }
+
+    /**
+     * Set the final content length for the page and flush the buffer
+     *
+     */
+    public function shutdown()
+    {
+        if ($this['config']->get('system.debugger.shutdown.close_connection')) {
+            //stop user abort
+            if (function_exists('ignore_user_abort')) {
+                @ignore_user_abort(true);
+            }
+
+            // close the session
+            if (isset($this['session'])) {
+                $this['session']->close();
+            }
+
+            // flush buffer if gzip buffer was started
+            if ($this['config']->get('system.cache.gzip')) {
+                ob_end_flush(); // gzhandler buffer
+            }
+
+            // get lengh and close the connection
+            header('Content-Length: ' . ob_get_length());
+            header("Connection: close");
+
+            // flush the regular buffer
+            ob_end_flush();
+            @ob_flush();
+            flush();
+
+            // fix for fastcgi close connection issue
+            if (function_exists('fastcgi_finish_request')) {
+                @fastcgi_finish_request();
+            }
+
+        }
+
+        $this->fireEvent('onShutdown');
+    }
+
+    /**
+     * This attempts to find media, other files, and download them
+     * @param $page
+     * @param $path
+     */
+    protected function fallbackUrl($page, $path)
+    {
+        /** @var Uri $uri */
+        $uri = $this['uri'];
+
+        /** @var Config $config */
+        $config = $this['config'];
+
+        $uri_extension = $uri->extension();
+
+        // Only allow whitelisted types to fallback
+        if (!in_array($uri_extension, $config->get('system.pages.fallback_types'))) {
+            return;
+        }
+
+        $path_parts = pathinfo($path);
+        $page = $this['pages']->dispatch($path_parts['dirname'], true);
+        if ($page) {
+            $media = $page->media()->all();
+
+            $parsed_url = parse_url(urldecode($uri->basename()));
+
+            $media_file = $parsed_url['path'];
+
+            // if this is a media object, try actions first
+            if (isset($media[$media_file])) {
+                $medium = $media[$media_file];
+                foreach ($uri->query(null, true) as $action => $params) {
+                    if (in_array($action, ImageMedium::$magic_actions)) {
+                        call_user_func_array(array(&$medium, $action), explode(',', $params));
+                    }
+                }
+                Utils::download($medium->path(), false);
+            }
+
+            // unsupported media type, try to download it...
+            if ($uri_extension) {
+                $extension = $uri_extension;
+            } else {
+                if (isset($path_parts['extension'])) {
+                    $extension = $path_parts['extension'];
+                } else {
+                    $extension = null;
+                }
+            }
+
+            if ($extension) {
+                $download = true;
+                if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) {
+                    $download = false;
+                }
+                Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
+            }
+        }
+    }
+}

+ 29 - 0
system/src/Grav/Common/GravTrait.php

@@ -0,0 +1,29 @@
+<?php
+namespace Grav\Common;
+
+trait GravTrait
+{
+    /**
+     * @var Grav
+     */
+    protected static $grav;
+
+    /**
+     * @return Grav
+     */
+    public static function getGrav()
+    {
+        if (!self::$grav) {
+            self::$grav = Grav::instance();
+        }
+        return self::$grav;
+    }
+
+    /**
+     * @param Grav $grav
+     */
+    public static function setGrav(Grav $grav)
+    {
+        self::$grav = $grav;
+    }
+}

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