Browse Source

first commit : grave core and admin plugin

Bachir Soussi Chiadmi 8 years ago
commit
ad38015ea9
100 changed files with 10993 additions and 0 deletions
  1. 63 0
      .htaccess
  2. 892 0
      CHANGELOG.md
  3. 21 0
      LICENSE
  4. 110 0
      README.md
  5. 0 0
      assets/.gitkeep
  6. 0 0
      backup/.gitkeep
  7. BIN
      bin/composer.phar
  8. 56 0
      bin/gpm
  9. 46 0
      bin/grav
  10. 0 0
      cache/.gitkeep
  11. 36 0
      composer.json
  12. 1012 0
      composer.lock
  13. 63 0
      htaccess.txt
  14. 0 0
      images/.gitkeep
  15. 42 0
      index.php
  16. 0 0
      logs/.gitkeep
  17. 87 0
      nginx.conf
  18. 2 0
      robots.txt
  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. BIN
      system/images/media/thumb-doc.png
  47. BIN
      system/images/media/thumb-gif.png
  48. BIN
      system/images/media/thumb-gz.png
  49. BIN
      system/images/media/thumb-html.png
  50. BIN
      system/images/media/thumb-jpeg.png
  51. BIN
      system/images/media/thumb-jpg.png
  52. BIN
      system/images/media/thumb-m4v.png
  53. BIN
      system/images/media/thumb-mov.png
  54. BIN
      system/images/media/thumb-mp4.png
  55. BIN
      system/images/media/thumb-pdf.png
  56. BIN
      system/images/media/thumb-png.png
  57. BIN
      system/images/media/thumb-swf.png
  58. BIN
      system/images/media/thumb-txt.png
  59. BIN
      system/images/media/thumb-zip.png
  60. 37 0
      system/languages/cs.yaml
  61. 43 0
      system/languages/de.yaml
  62. 94 0
      system/languages/en.yaml
  63. 60 0
      system/languages/fr.yaml
  64. 21 0
      system/languages/it.yaml
  65. 43 0
      system/languages/nl.yaml
  66. 43 0
      system/languages/ru.yaml
  67. 1143 0
      system/src/Grav/Common/Assets.php
  68. 130 0
      system/src/Grav/Common/Backup/ZipBackup.php
  69. 58 0
      system/src/Grav/Common/Browser.php
  70. 314 0
      system/src/Grav/Common/Cache.php
  71. 55 0
      system/src/Grav/Common/Composer.php
  72. 207 0
      system/src/Grav/Common/Config/Blueprints.php
  73. 479 0
      system/src/Grav/Common/Config/Config.php
  74. 186 0
      system/src/Grav/Common/Config/ConfigFinder.php
  75. 27 0
      system/src/Grav/Common/Config/Languages.php
  76. 456 0
      system/src/Grav/Common/Data/Blueprint.php
  77. 145 0
      system/src/Grav/Common/Data/Blueprints.php
  78. 240 0
      system/src/Grav/Common/Data/Data.php
  79. 68 0
      system/src/Grav/Common/Data/DataInterface.php
  80. 68 0
      system/src/Grav/Common/Data/DataMutatorTrait.php
  81. 672 0
      system/src/Grav/Common/Data/Validation.php
  82. 121 0
      system/src/Grav/Common/Debugger.php
  83. 52 0
      system/src/Grav/Common/Errors/Errors.php
  84. 52 0
      system/src/Grav/Common/Errors/Resources/error.css
  85. 30 0
      system/src/Grav/Common/Errors/Resources/layout.html.php
  86. 96 0
      system/src/Grav/Common/Errors/SimplePageHandler.php
  87. 73 0
      system/src/Grav/Common/File/CompiledFile.php
  88. 9 0
      system/src/Grav/Common/File/CompiledMarkdownFile.php
  89. 9 0
      system/src/Grav/Common/File/CompiledYamlFile.php
  90. 353 0
      system/src/Grav/Common/Filesystem/Folder.php
  91. 31 0
      system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
  92. 32 0
      system/src/Grav/Common/GPM/AbstractCollection.php
  93. 34 0
      system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php
  94. 21 0
      system/src/Grav/Common/GPM/Common/CachedCollection.php
  95. 42 0
      system/src/Grav/Common/GPM/Common/Package.php
  96. 398 0
      system/src/Grav/Common/GPM/GPM.php
  97. 343 0
      system/src/Grav/Common/GPM/Installer.php
  98. 17 0
      system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php
  99. 32 0
      system/src/Grav/Common/GPM/Local/Package.php
  100. 17 0
      system/src/Grav/Common/GPM/Local/Packages.php

+ 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.

+ 110 - 0
README.md

@@ -0,0 +1,110 @@
+# ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
+
+[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/getgrav/grav?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform.  There is **Zero** installation required.  Just extract the ZIP archive, and you are already up and running.  It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
+
+The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
+
+* [Twig Templating](http://twig.sensiolabs.org/): for powerful control of the user interface
+* [Markdown](http://en.wikipedia.org/wiki/Markdown): for easy content creation
+* [YAML](http://yaml.org): for simple configuration
+* [Parsedown](http://parsedown.org/): for fast Markdown and Markdown Extra support
+* [Doctrine Cache](http://docs.doctrine-project.org/en/2.0.x/reference/caching.html): layer for performance
+* [Pimple Dependency Injection Container](http://pimple.sensiolabs.org/): for extensibility and maintainability
+* [Symfony Event Dispatcher](http://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling
+* [Symfony Console](http://symfony.com/doc/current/components/console/introduction.html): for CLI interface
+* [Gregwar Image Library](https://github.com/Gregwar/Image): for dynamic image manipulation
+
+# Requirements
+
+- PHP 5.4 or higher. Check the [required modules list](http://learn.getgrav.org/basics/requirements#php-requirements)
+- Check the [Apache](http://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](http://learn.getgrav.org/basics/requirements#iis-requirements) requirements
+
+# QuickStart
+
+You have two options to get Grav:
+
+### Downloading a Grav Package
+
+You can download a **ready-built** package from the [Downloads page on http://getgrav.org](http://getgrav.org/downloads)
+
+### From GitHub
+
+1. Clone the Grav repository from [https://github.com/getgrav/grav]() to a folder in the webroot of your server, e.g. `~/webroot/grav`. Launch a **terminal** or **console** and navigate to the webroot folder:
+   ```
+   $ cd ~/webroot
+   $ git clone https://github.com/getgrav/grav.git
+   ```
+
+2. Install the **plugin** and **theme dependencies** by using the [Grav CLI application](http://learn.getgrav.org/advanced/grav-cli) `bin/grav`:
+   ```
+   $ cd ~/webroot/grav
+   $ bin/grav install
+   ```
+
+Check out the [install procedures](http://learn.getgrav.org/basics/installation) for more information.
+
+# Adding Functionality
+
+You can download [plugins](http://getgrav.org/downloads/plugins) or [themes](http://getgrav.org/downloads/themes) manually from the appropriate tab on the [Downloads page on http://getgrav.org](http://getgrav.org/downloads), but the preferred solution is to use the [Grav Package Manager](http://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
+
+```
+$ bin/gpm index
+```
+
+This will display all the available plugins and then you can install one or more with:
+
+```
+$ bin/gpm install <plugin/theme>
+```
+
+# Updating
+
+To update Grav you should use the [Grav Package Manager](http://learn.getgrav.org/advanced/grav-gpm) or `GPM`:
+
+```
+$ bin/gpm selfupgrade
+```
+
+To update plugins and themes:
+
+```
+$ bin/gpm update
+```
+
+
+# Contributing
+We appreciate any contribution to Grav, whether it is related to bugs, grammar, or simply a suggestion or improvement.
+However, we ask that any contributions follow our simple guidelines in order to be properly received.
+
+All our projects follow the [GitFlow branching model][gitflow-model], from development to release. If you are not familiar with it, there are several guides and tutorials to make you understand what it is about.
+
+You will probably want to get started by installing [this very good collection of git extensions][gitflow-extensions].
+
+What you mainly want to know is that:
+
+- All the main activity happens in the `develop` branch. Any pull request should be addressed only to that branch. We will not consider pull requests made to the `master`.
+- It's very well appreciated, and highly suggested, to start a new feature whenever you want to make changes or add functionalities. It will make it much easier for us to just checkout your feature branch and test it, before merging it into `develop`
+
+# Getting Started
+
+* [What is Grav?](http://learn.getgrav.org/basics/what-is-grav)
+* [Install](http://learn.getgrav.org/basics/installation) Grav in few seconds
+* Understand the [Configuration](http://learn.getgrav.org/basics/grav-configuration)
+* Take a peek at our available free [Skeletons](http://getgrav.org/downloads/skeletons#extras)
+* If you have questions, check out `#grav` on irc.freenode.net
+* Have fun!
+
+# Exploring more
+
+* Have a look at our [Basic Tutorial](http://learn.getgrav.org/basics/basic-tutorial)
+* Dive into more [advanced](http://learn.getgrav.org/advanced) functions
+
+# License
+
+See [LICENSE](LICENSE)
+
+
+[gitflow-model]: http://nvie.com/posts/a-successful-git-branching-model/
+[gitflow-extensions]: https://github.com/nvie/gitflow

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

+ 0 - 0
cache/.gitkeep


+ 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": []
+}

+ 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

+ 0 - 0
images/.gitkeep


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

+ 0 - 0
logs/.gitkeep


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

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

BIN
system/images/media/thumb-doc.png


BIN
system/images/media/thumb-gif.png


BIN
system/images/media/thumb-gz.png


BIN
system/images/media/thumb-html.png


BIN
system/images/media/thumb-jpeg.png


BIN
system/images/media/thumb-jpg.png


BIN
system/images/media/thumb-m4v.png


BIN
system/images/media/thumb-mov.png


BIN
system/images/media/thumb-mp4.png


BIN
system/images/media/thumb-pdf.png


BIN
system/images/media/thumb-png.png


BIN
system/images/media/thumb-swf.png


BIN
system/images/media/thumb-txt.png


BIN
system/images/media/thumb-zip.png


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

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