diff --git a/.htaccess b/.htaccess index db5015937..55bcd08a7 100644 --- a/.htaccess +++ b/.htaccess @@ -44,15 +44,17 @@ RewriteRule .* index.php [L] ## 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] +RewriteRule ^(.git|cache|bin|logs|backup)/(.*) error [F] +# Block access to specific file types for these system folders +RewriteRule ^(system|vendor)/(.*)\.(txt|xml|md|html|yaml|php|pl|py|cgi|twig|sh|bat)$ error [F] +# Block access to specific file types for these user folders +RewriteRule ^(user)/(.*)\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ error [F] # Block all direct access to .md files: -RewriteRule \.md$ error [L] +RewriteRule \.md$ error [F] # 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] +RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] ## End - Security diff --git a/CHANGELOG.md b/CHANGELOG.md index aad2c4b8a..4300bc74b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,30 @@ +# 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 -## xx/xx/2015 +## 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 @@ -11,8 +32,10 @@ * 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 diff --git a/README.md b/README.md index 83daee5d3..8a405e3dc 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ The underlying architecture of Grav is designed to use well-established and _bes * [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 Mardown Extra support +* [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 Dispacher](http://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling +* [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 @@ -53,7 +53,7 @@ You can download [plugins](http://getgrav.org/downloads/plugins) or [themes](htt $ bin/gpm index ``` -This will display all the available plugins and then you can install one ore more with: +This will display all the available plugins and then you can install one or more with: ``` $ bin/gpm install @@ -76,7 +76,7 @@ $ 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 contribution follow our simple guidelines in order to be properly received. +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. diff --git a/composer.lock b/composer.lock index 7b094c5e9..d3a9d25b8 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "doctrine/cache", - "version": "v1.4.2", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca" + "reference": "eb8a73619af4f1c8711e2ce482f5de3643258a1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca", - "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca", + "url": "https://api.github.com/repos/doctrine/cache/zipball/eb8a73619af4f1c8711e2ce482f5de3643258a1f", + "reference": "eb8a73619af4f1c8711e2ce482f5de3643258a1f", "shasum": "" }, "require": { @@ -38,8 +38,8 @@ } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Cache\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" } }, "notification-url": "https://packagist.org/downloads/", @@ -74,7 +74,7 @@ "cache", "caching" ], - "time": "2015-08-31 12:36:41" + "time": "2015-10-28 11:27:45" }, { "name": "donatj/phpuseragentparser", @@ -714,16 +714,16 @@ }, { "name": "symfony/console", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "06cb17c013a82f94a3d840682b49425cd00a2161" + "reference": "5efd632294c8320ea52492db22292ff853a43766" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/06cb17c013a82f94a3d840682b49425cd00a2161", - "reference": "06cb17c013a82f94a3d840682b49425cd00a2161", + "url": "https://api.github.com/repos/symfony/console/zipball/5efd632294c8320ea52492db22292ff853a43766", + "reference": "5efd632294c8320ea52492db22292ff853a43766", "shasum": "" }, "require": { @@ -732,7 +732,6 @@ "require-dev": { "psr/log": "~1.0", "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", "symfony/process": "~2.1" }, "suggest": { @@ -767,20 +766,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2015-09-25 08:32:23" + "time": "2015-10-20 14:38:46" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9" + "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", - "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87a5db5ea887763fa3a31a5471b512ff1596d9b8", + "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8", "shasum": "" }, "require": { @@ -791,7 +790,6 @@ "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": { @@ -825,28 +823,25 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-09-22 13:49:29" + "time": "2015-10-11 09:39:48" }, { "name": "symfony/var-dumper", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "ba8c9a0edf18f70a7efcb8d3eb35323a10263338" + "reference": "eb033050050916b6bfa51be71009ef67b16046c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ba8c9a0edf18f70a7efcb8d3eb35323a10263338", - "reference": "ba8c9a0edf18f70a7efcb8d3eb35323a10263338", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/eb033050050916b6bfa51be71009ef67b16046c9", + "reference": "eb033050050916b6bfa51be71009ef67b16046c9", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "suggest": { "ext-symfony_debug": "" }, @@ -884,28 +879,25 @@ "debug", "dump" ], - "time": "2015-09-22 14:41:01" + "time": "2015-10-25 17:17:38" }, { "name": "symfony/yaml", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770" + "reference": "eca9019c88fbe250164affd107bc8057771f3f4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770", - "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770", + "url": "https://api.github.com/repos/symfony/yaml/zipball/eca9019c88fbe250164affd107bc8057771f3f4d", + "reference": "eca9019c88fbe250164affd107bc8057771f3f4d", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { @@ -933,20 +925,20 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-09-14 14:14:09" + "time": "2015-10-11 09:39:48" }, { "name": "twig/twig", - "version": "v1.22.3", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ebfc36b7e77b0c1175afe30459cf943010245540" + "reference": "5868cd822fd6cf626d5f805439575f9c323cee2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ebfc36b7e77b0c1175afe30459cf943010245540", - "reference": "ebfc36b7e77b0c1175afe30459cf943010245540", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/5868cd822fd6cf626d5f805439575f9c323cee2a", + "reference": "5868cd822fd6cf626d5f805439575f9c323cee2a", "shasum": "" }, "require": { @@ -959,7 +951,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } }, "autoload": { @@ -994,7 +986,7 @@ "keywords": [ "templating" ], - "time": "2015-10-13 07:07:02" + "time": "2015-10-29 23:29:01" } ], "packages-dev": [], diff --git a/htaccess.txt b/htaccess.txt index db5015937..55bcd08a7 100644 --- a/htaccess.txt +++ b/htaccess.txt @@ -44,15 +44,17 @@ RewriteRule .* index.php [L] ## 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] +RewriteRule ^(.git|cache|bin|logs|backup)/(.*) error [F] +# Block access to specific file types for these system folders +RewriteRule ^(system|vendor)/(.*)\.(txt|xml|md|html|yaml|php|pl|py|cgi|twig|sh|bat)$ error [F] +# Block access to specific file types for these user folders +RewriteRule ^(user)/(.*)\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ error [F] # Block all direct access to .md files: -RewriteRule \.md$ error [L] +RewriteRule \.md$ error [F] # 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] +RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] ## End - Security diff --git a/nginx.conf b/nginx.conf index 5ce2521b4..d325ff308 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,87 +1,44 @@ -worker_processes 1; +server { + #listen 80; + index index.html index.php; -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; - } + ## Begin - Server Info + root /home/user/www/html; + server_name localhost; + ## End - Server Info + ## Begin - Index + # for subfolders, simply adjust: + # `location /subfolder {` + # and the rewrite to use `/subfolder/index.php` + location / { + try_files $uri $uri/ /index.html; + if (!-e $request_filename){ rewrite ^(.*)$ /index.php last; } } + ## End - Index + ## Begin - PHP + location ~ \.php$ { + # Choose either a socket or TCP/IP address + fastcgi_pass unix:/var/run/php5-fpm.sock; + # fastcgi_pass 127.0.0.1:9000; + + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + } + ## End - PHP + + ## Begin - Security + # deny all direct access for these folders + location ~* /(.git|cache|bin|logs|backups)/.*$ { return 403; } + # deny running scripts inside core system folders + location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; } + # deny running scripts inside user folder + location ~* /user/.*\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; } + # deny access to specific files in the root folder + location ~ /(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess) { return 403; } + ## End - Security } + diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index b17fc3605..e79da93e3 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -215,6 +215,15 @@ form: 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 @@ -774,6 +783,7 @@ form: param_sep: type: select + size: medium label: PLUGIN_ADMIN.PARAMETER_SEPARATOR classes: fancy help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index 5ccd5f641..83674c721 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -29,7 +29,7 @@ form: size: large label: PLUGIN_ADMIN.PASSWORD validate: - required: true + required: false message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE pattern: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' diff --git a/system/config/streams.yaml b/system/config/streams.yaml index c06ec3547..9053b9080 100644 --- a/system/config/streams.yaml +++ b/system/config/streams.yaml @@ -1,9 +1,4 @@ schemes: - asset: - type: ReadOnlyStream - paths: - - assets - image: type: ReadOnlyStream paths: diff --git a/system/config/system.yaml b/system/config/system.yaml index 443788d67..43b1dbba5 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -49,11 +49,12 @@ pages: 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 + 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 @@ -71,6 +72,7 @@ twig: autoescape: false # Autoescape Twig vars undefined_functions: true # Allow undefined functions undefined_filters: true # Allow undefined filters + umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 assets: # Configuration for Assets Manager (JS, CSS) css_pipeline: false # The CSS pipeline is the unification of multiple CSS resources into one file diff --git a/system/defines.php b/system/defines.php index 2adc2b99e..ff7cf777e 100644 --- a/system/defines.php +++ b/system/defines.php @@ -2,7 +2,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.0.0-rc.1'); +define('GRAV_VERSION', '1.0.0-rc.4'); define('DS', '/'); // Directories and Paths @@ -13,14 +13,14 @@ define('ROOT_DIR', GRAV_ROOT . '/'); define('USER_PATH', 'user/'); define('USER_DIR', ROOT_DIR . USER_PATH); define('SYSTEM_DIR', ROOT_DIR .'system/'); -define('ASSETS_DIR', ROOT_DIR . 'assets/'); define('CACHE_DIR', ROOT_DIR . 'cache/'); -define('IMAGES_DIR', ROOT_DIR . 'images/'); define('LOG_DIR', ROOT_DIR .'logs/'); -define('ACCOUNTS_DIR', USER_DIR .'accounts/'); -define('PAGES_DIR', USER_DIR .'pages/'); // 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/'); diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php index f1a61b349..50e4ba4a2 100644 --- a/system/src/Grav/Common/Assets.php +++ b/system/src/Grav/Common/Assets.php @@ -74,6 +74,8 @@ class Assets protected $config; protected $base_url; protected $timestamp = ''; + protected $assets_dir; + protected $assets_url; // Default values for pipeline settings protected $css_minify = true; @@ -117,7 +119,7 @@ class Assets } // Pipeline requires public dir - if (($this->js_pipeline || $this->css_pipeline) && !is_dir(ASSETS_DIR)) { + if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) { throw new \Exception('Assets: Public dir not found'); } @@ -175,6 +177,11 @@ class Assets $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 . '/'; @@ -621,8 +628,8 @@ class Assets $file = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group) . '.css'; - $relative_path = "{$this->base_url}" . basename(ASSETS_DIR) . "/{$file}"; - $absolute_path = ASSETS_DIR . $file; + $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)) { @@ -689,8 +696,8 @@ class Assets $file = md5(json_encode($this->js) . $this->js_minify . $group) . '.js'; - $relative_path = "{$this->base_url}" . basename(ASSETS_DIR) . "/{$file}"; - $absolute_path = ASSETS_DIR . $file; + $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)) { @@ -852,12 +859,12 @@ class Assets public function addDir($directory, $pattern = self::DEFAULT_REGEX) { // Check if public_dir exists - if (!is_dir(ASSETS_DIR)) { + if (!is_dir($this->assets_dir)) { throw new Exception('Assets: Public dir not found'); } // Get files - $files = $this->rglob(ASSETS_DIR . DIRECTORY_SEPARATOR . $directory, $pattern, ASSETS_DIR); + $files = $this->rglob($this->assets_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $this->assets_dir); // No luck? Nothing to do if (!$files) { diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php index 0445c502b..6ba75f4a3 100644 --- a/system/src/Grav/Common/Cache.php +++ b/system/src/Grav/Common/Cache.php @@ -21,6 +21,8 @@ use Grav\Common\Filesystem\Folder; */ class Cache extends Getters { + use GravTrait; + /** * @var string Cache key. */ @@ -44,30 +46,30 @@ class Cache extends Getters protected $cache_dir; protected static $standard_remove = [ - 'cache/twig/', - 'cache/doctrine/', - 'cache/compiled/', - 'cache/validated-', - 'images/', - 'assets/', + 'cache://twig/', + 'cache://doctrine/', + 'cache://compiled/', + 'cache://validated-', + 'cache://images', + 'asset://', ]; protected static $all_remove = [ - 'cache/', - 'images/', - 'assets/' + 'cache://', + 'cache://images', + 'asset://' ]; protected static $assets_remove = [ - 'assets/' + 'asset://' ]; protected static $images_remove = [ - 'images/' + 'cache://images' ]; protected static $cache_remove = [ - 'cache/' + 'cache://' ]; /** @@ -221,7 +223,7 @@ class Cache extends Getters */ public static function clearCache($remove = 'standard') { - + $locator = self::getGrav()['locator']; $output = []; $user_config = USER_DIR . 'config/system.yaml'; @@ -243,10 +245,16 @@ class Cache extends Getters } - foreach ($remove_paths as $path) { + foreach ($remove_paths as $stream) { + + // Convert stream to a real path + $path = $locator->findResource($stream, true, true); + // Make sure path exists before proceeding, otherwise we would wipe ROOT_DIR + if (!$path) + throw new \RuntimeException("Stream '{$stream}' not found", 500); $anything = false; - $files = glob(ROOT_DIR . $path . '*'); + $files = glob($path . '/*'); if (is_array($files)) { foreach ($files as $file) { @@ -263,7 +271,7 @@ class Cache extends Getters } if ($anything) { - $output[] = 'Cleared: ' . $path . '*'; + $output[] = 'Cleared: ' . $path . '/*'; } } diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index 9e69e994c..fe8105ddf 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -30,6 +30,12 @@ class Config extends Data '' => ['user'], ] ], + 'asset' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['assets'], + ] + ], 'blueprints' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ @@ -194,7 +200,7 @@ class Config extends Data $checkConfig = $this->get('system.cache.check.config', true); $checkSystem = $this->get('system.cache.check.system', true); - if (!$checkBlueprints && !!$checkLanguages && $checkConfig && !$checkSystem) { + if (!$checkBlueprints && !$checkLanguages && !$checkConfig && !$checkSystem) { $this->messages[] = 'Skip configuration timestamp check.'; return false; } diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 15cdce65c..4f24620af 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -76,7 +76,8 @@ class Blueprint try { $this->validateArray($data, $this->nested); } catch (\RuntimeException $e) { - throw new \RuntimeException(sprintf('Validation failed: %s', $e->getMessage())); + $language = self::getGrav()['language']; + throw new \RuntimeException(sprintf('Validation failed: %s', $language->translate($e->getMessage()))); } } diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php index 2a12eb22d..6a35fbc9e 100644 --- a/system/src/Grav/Common/Data/Data.php +++ b/system/src/Grav/Common/Data/Data.php @@ -224,16 +224,6 @@ class Data implements DataInterface return $this->file()->raw(); } - /** - * Return the data items. - * - * @return array - */ - public function items() - { - return $this->items; - } - /** * Set or get the data storage. * diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 8bdc064af..3382a7218 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -588,6 +588,10 @@ class Validation public static function validateRequired($value, $params) { + if (is_string($value)) { + $value = trim($value); + } + return (bool) $params !== true || !empty($value); } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index cbf81eecb..da1ea4217 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -114,7 +114,7 @@ class Grav extends Container /** @var Uri $uri */ $uri = $c['uri']; - $path = rtrim($uri->path(), '/'); + $path = $uri->path(); // Don't trim to support trailing slash default routes $path = $path ?: '/'; $page = $pages->dispatch($path); @@ -296,7 +296,10 @@ class Grav extends Container if ($uri->isExternal($route)) { $url = $route; } else { - $url = rtrim($uri->rootUrl(), '/') .'/'. trim($route, '/'); + if ($this['config']->get('system.pages.redirect_trailing_slash', true)) + $url = rtrim($uri->rootUrl(), '/') .'/'. trim($route, '/'); // Remove trailing slash + else + $url = rtrim($uri->rootUrl(), '/') .'/'. ltrim($route, '/'); // Support trailing slash default routes } header("Location: {$url}", true, $code); @@ -457,6 +460,16 @@ class Grav extends Container /** @var Uri $uri */ $uri = $this['uri']; + /** @var Config $config */ + $config = $this['config']; + + $uri_extension = $uri->extension(); + + // Only allow whitelisted types to fallback + if (!in_array($uri_extension, $config->get('system.pages.fallback_types'))) { + return; + } + $path_parts = pathinfo($path); $page = $this['pages']->dispatch($path_parts['dirname'], true); if ($page) { @@ -478,7 +491,6 @@ class Grav extends Container } // unsupported media type, try to download it... - $uri_extension = $uri->extension(); if ($uri_extension) { $extension = $uri_extension; } else { @@ -491,7 +503,7 @@ class Grav extends Container if ($extension) { $download = true; - if (in_array(ltrim($extension, '.'), $this['config']->get('system.media.unsupported_inline_types', []))) { + if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) { $download = false; } Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download); diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php index 4f6718836..5dfe343f2 100644 --- a/system/src/Grav/Common/Language/Language.php +++ b/system/src/Grav/Common/Language/Language.php @@ -162,7 +162,7 @@ class Language */ public function setActiveFromUri($uri) { - $regex = '/(^\/(' . $this->getAvailable() . ')).*/'; + $regex = '/(^\/(' . $this->getAvailable() . '))(?:\/.*|$)/i'; // if languages set if ($this->enabled()) { diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php index 4e85b419f..cc1593c28 100644 --- a/system/src/Grav/Common/Page/Collection.php +++ b/system/src/Grav/Common/Page/Collection.php @@ -36,6 +36,18 @@ class Collection extends Iterator return $this->params; } + /** + * Add a single page to a collection + * + * @param Page $page + * @return $this + */ + public function addPage(Page $page) + { + $this->items[$page->path()] = ['slug' => $page->slug()]; + return $this; + } + /** * * Create a copy of this collection @@ -96,6 +108,7 @@ class Collection extends Iterator * Remove item from the list. * * @param Page|string|null $key + * @return $this|void * @throws \InvalidArgumentException */ public function remove($key = null) @@ -110,6 +123,7 @@ class Collection extends Iterator } parent::remove($key); + return $this; } /** diff --git a/system/src/Grav/Common/Page/Medium/ImageFile.php b/system/src/Grav/Common/Page/Medium/ImageFile.php index bb9ce59aa..e75f6d979 100644 --- a/system/src/Grav/Common/Page/Medium/ImageFile.php +++ b/system/src/Grav/Common/Page/Medium/ImageFile.php @@ -9,6 +9,14 @@ class ImageFile extends \Gregwar\Image\Image { use GravTrait; + /** + * Clear previously applied operations + */ + public function clearOperations() + { + $this->operations = []; + } + /** * This is the same as the Gregwar Image class except this one fires a Grav Event on creation of new cached file * diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index c38d8e874..c9c5dcc07 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -260,6 +260,7 @@ class ImageMedium extends Medium if ($this->image) { $this->image(); + $this->image->clearOperations(); // Clear previously applied operations $this->filter(); } @@ -359,6 +360,48 @@ class ImageMedium extends Medium return empty($this->sizes) ? '100vw' : $this->sizes; } + /** + * Allows to set the width attribute from Markdown or Twig + * Examples: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ page.media['myimg.png'].width().height().html }} + * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }} + * + * @param mixed $value A value or 'auto' or empty to use the width of the image + * @return $this + */ + public function width($value = 'auto') + { + if (!$value || $value == 'auto') + $this->attributes['width'] = $this->get('width'); + else + $this->attributes['width'] = $value; + return $this; + } + + /** + * Allows to set the height attribute from Markdown or Twig + * Examples: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ page.media['myimg.png'].width().height().html }} + * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }} + * + * @param mixed $value A value or 'auto' or empty to use the height of the image + * @return $this + */ + public function height($value = 'auto') + { + if (!$value || $value == 'auto') + $this->attributes['height'] = $this->get('height'); + else + $this->attributes['height'] = $value; + return $this; + } + /** * Forward the call to the image processing method. * diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php index d3d3e6298..b36864c27 100644 --- a/system/src/Grav/Common/Page/Medium/Medium.php +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -209,9 +209,13 @@ class Medium extends Data implements RenderableInterface $style = ''; foreach ($this->styleAttributes as $key => $value) { - $style .= $key . ': ' . $value . ';'; + if (is_numeric($key)) // Special case for inline style attributes, refer to style() method + $style .= $value; + else + $style .= $key . ': ' . $value . ';'; } - $attributes['style'] = $style; + if ($style) + $attributes['style'] = $style; if (empty($attributes['title'])) { if (!empty($title)) { @@ -388,6 +392,19 @@ class Medium extends Data implements RenderableInterface return $this->link($reset, $attributes); } + /** + * Allows to add an inline style attribute from Markdown or Twig + * Example: ![Example](myimg.png?style=float:left) + * + * @param string $style + * @return $this + */ + public function style($style) + { + $this->styleAttributes[] = rtrim($style, ';') . ';'; + return $this; + } + /** * Allow any action to be called on this medium from twig or markdown * @@ -440,4 +457,5 @@ class Medium extends Data implements RenderableInterface return $this->_thumbnail; } + } diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 80aa8bab7..f2c6ea2d9 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -1935,7 +1935,7 @@ class Page // Format: @command.param $cmd = $value; $params = array(); - } elseif (is_array($value) && count($value) == 1) { + } elseif (is_array($value) && count($value) == 1 && !is_int(key($value))) { // Format: @command.param: { attr1: value1, attr2: value2 } $cmd = (string) key($value); $params = (array) current($value); @@ -1957,51 +1957,92 @@ class Page return $value; } + /** @var Pages $pages */ + $pages = self::getGrav()['pages']; + $parts = explode('.', $cmd); $current = array_shift($parts); - $results = null; + $results = new Collection(); switch ($current) { case '@self': if (!empty($parts)) { switch ($parts[0]) { case 'modular': + // @self.modular: false (alternative to @self.children) if (!empty($params) && $params[0] === false) { - $results = $this->children()->nonModular()->published(); + $results = $this->children()->nonModular(); break; } - $results = $this->children()->modular()->published(); + $results = $this->children()->modular(); break; case 'children': - $results = $this->children()->nonModular()->published(); + $results = $this->children()->nonModular(); + break; + + case 'parent': + $collection = new Collection(); + $results = $collection->addPage($this->parent()); + break; + + case 'siblings': + $results = $this->parent()->children()->remove($this->path()); + break; + + case 'descendants': + $results = $pages->all($this)->remove($this->path())->nonModular(); break; } } + + $results = $results->published(); break; case '@page': + $page = null; + if (!empty($params)) { - /** @var Pages $pages */ - $pages = self::getGrav()['pages']; + $page = $this->find($params[0]); + } - list($what, $recurse) = array_pad($params, 2, null); + // safety check in case page is not found + if (!isset($page)) { + return $results; + } - if ($what == '@root') { - $page = $pages->root(); - } else { - $page = $this->find($what); + // Handle a @page.descendants + if (!empty($parts)) { + switch ($parts[0]) { + case 'self': + $results = new Collection(); + $results = $results->addPage($page); + break; + + case 'descendants': + $results = $pages->all($page)->remove($page->path()); + break; + + case 'children': + $results = $page->children(); + break; } + } else { + $results = $page->children(); + } - if ($page) { - if ($recurse) { - $results = $pages->all($page)->nonModular()->published(); - } else { - $results = $page->children()->nonModular()->published(); - } - } + $results = $results->nonModular()->published(); + + break; + + case '@root': + if (!empty($parts) && $parts[0] == 'descendants') { + $results = $pages->all($pages->root())->nonModular()->published(); + } else { + $results = $pages->root()->children()->nonModular()->published(); } break; + case '@taxonomy': // Gets a collection of pages by using one of the following formats: // @taxonomy.category: blog diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 28e984ef9..36393a696 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -272,6 +272,10 @@ class Pages { // Fetch page if there's a defined route to it. $page = isset($this->routes[$url]) ? $this->get($this->routes[$url]) : null; + // Try without trailing slash + if (!$page && Utils::endsWith($url, '/')) { + $page = isset($this->routes[rtrim($url, '/')]) ? $this->get($this->routes[rtrim($url, '/')]) : null; + } // Are we in the admin? this is important! $not_admin = !isset($this->grav['admin']); diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index 1b9051c01..add87d3be 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -171,6 +171,8 @@ class Themes extends Iterator exit("Theme '$name' does not exist, unable to display page."); } + $this->config->set('theme', $config->get('themes.' . $name)); + if (empty($class)) { $class = new Theme($grav, $config, $name); } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 92648df7a..0473d3938 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -133,11 +133,15 @@ class Uri // set the original basename $this->basename = $parts['basename']; + // set the extension + if (isset($parts['extension'])) { + $this->extension = $parts['extension']; + } + $valid_page_types = implode('|', $config->get('system.pages.types')); if (preg_match("/\.(".$valid_page_types.")$/", $parts['basename'])) { $uri = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS). '/' .$parts['filename']; - $this->extension = $parts['extension']; } // set the new url