diff --git a/.travis.yml b/.travis.yml index 08127e7a2a..19df5390d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: php php: - - '5.5' - '5.6' - '7.0.21' - '7.1' diff --git a/CHANGELOG.md b/CHANGELOG.md index 93342d1dd7..16feced8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ +# v1.5.0 +## 08/17/2018 + +1. [](#new) + * Set minimum requirements to [PHP 5.6.4](https://getgrav.org/blog/raising-php-requirements-2018) + * Updated Doctrine Collections to 1.4 + * Updated Symfony Components to 3.4 (with compatibility mode to fall back to Symfony YAML 2.8) + * Added `Uri::method()` to get current HTTP method (GET/POST etc) + * `FormatterInterface`: Added `getSupportedFileExtensions()` and `getDefaultFileExtension()` methods + * Added option to disable `SimpleCache` key validation + * Added support for multiple repo locations for `bin/grav install` command + * Added twig filters for casting values: `|string`, `|int`, `|bool`, `|float`, `|array` + * Made `ObjectCollection::matching()` criteria expressions to behave more like in Twig + * Criteria: Added support for `LENGTH()`, `LOWER()`, `UPPER()`, `LTRIM()`, `RTRIM()` and `TRIM()` + * Added `Grav\Framework\File\Formatter` classes for encoding/decoding YAML, Markdown, JSON, INI and PHP serialized strings + * Added `Grav\Framework\Session` class to replace `RocketTheme\Toolbox\Session\Session` + * Added `Grav\Common\Media` interfaces and trait; use those in `Page` and `Media` classes + * Added `Grav\Common\Page` interface to allow custom page types in the future + * Added setting to disable sessions from the site [#2013](https://github.com/getgrav/grav/issues/2013) + * Added new `strict_mode` settings in `system.yaml` for compatibility +1. [](#improved) + * Improved `Utils::url()` to support query strings + * Display better exception message if Grav fails to initialize + * Added `muted` and `playsinline` support to videos [#2124](https://github.com/getgrav/grav/pull/2124) + * Added `MediaTrait::clearMediaCache()` to allow cache to be cleared + * Added `MediaTrait::getMediaCache()` to allow custom caching + * Improved session handling, allow all session configuration options in `system.session.options` +1. [](#bugfix) + * Fix broken form nonce logic [#2121](https://github.com/getgrav/grav/pull/2121) + * Fixed issue with uppercase extensions and fallback media URLs [#2133](https://github.com/getgrav/grav/issues/2133) + * Fixed theme inheritance issue with `camel-case` that includes numbers [#2134](https://github.com/getgrav/grav/issues/2134) + * Typo in demo typography page [#2136](https://github.com/getgrav/grav/pull/2136) + * Fix for incorrect plugin order in debugger panel + * Made `|markdown` filter HTML safe + * Fixed bug in `ContentBlock` serialization + * Fixed `Route::withQueryParam()` to accept array values + * Fixed typo in truncate function [#1943](https://github.com/getgrav/grav/issues/1943) + * Fixed blueprint field validation: Allow numeric inputs in text fields + +# v1.4.8 +## 07/31/2018 + +1. [](#improved) + * Add Grav version to debug bar messages tab [#2106](https://github.com/getgrav/grav/pull/2106) + * Add Nginx config for ddev project to `webserver-configs` [#2117](https://github.com/getgrav/grav/pull/2117) + * Vendor library updates +1. [](#bugfix) + * Don't allow `null` to be set as Page content + +# v1.4.7 +## 07/13/2018 + +1. [](#improved) + * Use `getFilename` instead of `getBasename` [#2087](https://github.com/getgrav/grav/issues/2087) +1. [](#bugfix) + * Fix for modular page preview [#2066](https://github.com/getgrav/grav/issues/2066) + * `Page::routeCanonical()` should be string not array [#2069](https://github.com/getgrav/grav/issues/2069) + # v1.4.6 -## 06/19/2018 +## 06/20/2018 1. [](#improved) * Manually re-added the improved SSL off-loading that was lost with Grav v1.4.0 merge [#1888](https://github.com/getgrav/grav/pull/1888) @@ -13,6 +71,8 @@ * Fix classes on non-http based protocol links [#2034](https://github.com/getgrav/grav/issues/2034) * Fixed crash on IIS (Windows) with open_basedir in effect [#2053](https://github.com/getgrav/grav/issues/2053) * Fixed incorrect routing with setup.php based base [#1892](https://github.com/getgrav/grav/issues/1892) + * Fixed image resource memory deallocation [#2045](https://github.com/getgrav/grav/pull/2045) + * Fixed issue with Errors `display:` option not handling integers properly [admin#1452](https://github.com/getgrav/grav-plugin-admin/issues/1452) # v1.4.5 ## 05/15/2018 @@ -21,7 +81,6 @@ * Fixed an issue with some users getting **2FA** prompt after upgrade [admin#1442](https://github.com/getgrav/grav-plugin-admin/issues/1442) * Do not crash when generating URLs with arrays as parameters [#2018](https://github.com/getgrav/grav/pull/2018) * Utils::truncateHTML removes whitespace when generating summaries [#2004](https://github.com/getgrav/grav/pull/2004) - * Fixed issue with Errors `display:` option not handling integers properly [admin#1452](https://github.com/getgrav/grav-plugin-admin/issues/1452) # v1.4.4 ## 05/11/2018 diff --git a/README.md b/README.md index 916421930f..5afbe43657 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The underlying architecture of Grav is designed to use well-established and _bes # Requirements -- PHP 5.5.9 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements) +- PHP 5.6.4 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements) - Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements # QuickStart diff --git a/composer.json b/composer.json index 9918530bfc..fbd5106f72 100644 --- a/composer.json +++ b/composer.json @@ -6,17 +6,17 @@ "homepage": "http://getgrav.org", "license": "MIT", "require": { - "php": ">=5.5.9", + "php": ">=5.6.4", "twig/twig": "~1.24", "erusev/parsedown": "1.6.4", "erusev/parsedown-extra": "~0.7", - "symfony/yaml": "~2.8", - "symfony/console": "~2.8", - "symfony/event-dispatcher": "~2.8", - "symfony/var-dumper": "~2.8", + "symfony/yaml": "~3.4", + "symfony/console": "~3.4", + "symfony/event-dispatcher": "~3.4", + "symfony/var-dumper": "~3.4", "symfony/polyfill-iconv": "~1.0", "doctrine/cache": "^1.6", - "doctrine/collections": "1.3", + "doctrine/collections": "^1.4", "psr/simple-cache": "^1.0", "psr/http-message": "^1.0", "guzzlehttp/psr7": "^1.4", @@ -26,7 +26,7 @@ "gregwar/image": "2.*", "donatj/phpuseragentparser": "~0.3", "pimple/pimple": "~3.2", - "rockettheme/toolbox": "~1.3", + "rockettheme/toolbox": "~1.4", "maximebf/debugbar": "~1.10", "ext-mbstring": "*", "ext-openssl": "*", @@ -45,7 +45,7 @@ }, "config": { "platform": { - "php": "5.5.9" + "php": "5.6.4" } }, "repositories": [ diff --git a/composer.lock b/composer.lock index cb8fe41297..d1e051c567 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c71dffc7daccd08aba7a52a476569d4c", + "content-hash": "ec4860b0ab68318d0e4550d58b5c12b3", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -52,16 +52,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169" + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169", - "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", "shasum": "" }, "require": { @@ -104,7 +104,7 @@ "ssl", "tls" ], - "time": "2018-03-29T19:57:20+00:00" + "time": "2018-08-08T08:57:40+00:00" }, { "name": "doctrine/cache", @@ -178,28 +178,29 @@ }, { "name": "doctrine/collections", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -240,20 +241,20 @@ "collections", "iterator" ], - "time": "2015-04-14T22:21:58+00:00" + "time": "2017-01-03T10:49:41+00:00" }, { "name": "donatj/phpuseragentparser", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/donatj/PhpUserAgent.git", - "reference": "ea79de6a18e52285e62cd75cf1cebe276ecaf503" + "reference": "9de58cc2a3e986bfee7f4cea3365c830b765cf65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/ea79de6a18e52285e62cd75cf1cebe276ecaf503", - "reference": "ea79de6a18e52285e62cd75cf1cebe276ecaf503", + "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/9de58cc2a3e986bfee7f4cea3365c830b765cf65", + "reference": "9de58cc2a3e986bfee7f4cea3365c830b765cf65", "shasum": "" }, "require": { @@ -291,7 +292,7 @@ "user agent", "useragent" ], - "time": "2017-10-23T16:52:52+00:00" + "time": "2018-06-21T15:54:46+00:00" }, { "name": "erusev/parsedown", @@ -605,26 +606,30 @@ }, { "name": "league/climate", - "version": "3.2.4", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/climate.git", - "reference": "ca70f67f7739cca823eba0ad98f8130bca226bf0" + "reference": "d657a19837c1f79a891381fb128b755aa3386381" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/climate/zipball/ca70f67f7739cca823eba0ad98f8130bca226bf0", - "reference": "ca70f67f7739cca823eba0ad98f8130bca226bf0", + "url": "https://api.github.com/repos/thephpleague/climate/zipball/d657a19837c1f79a891381fb128b755aa3386381", + "reference": "d657a19837c1f79a891381fb128b755aa3386381", "shasum": "" }, "require": { - "php": ">=5.4.0", - "seld/cli-prompt": "~1.0" + "php": "^5.6|^7.0", + "psr/log": "^1.0", + "seld/cli-prompt": "^1.0" }, "require-dev": { - "mikey179/vfsstream": "~1.4", - "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.6" + "mikey179/vfsstream": "^1.4", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^5.7.16" + }, + "suggest": { + "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" }, "type": "library", "autoload": { @@ -637,6 +642,12 @@ "MIT" ], "authors": [ + { + "name": "Craig Duncan", + "email": "git@duncanc.co.uk", + "homepage": "https://github.com/duncan3dc", + "role": "Developer" + }, { "name": "Joe Tannenbaum", "email": "hey@joe.codes", @@ -652,7 +663,7 @@ "php", "terminal" ], - "time": "2016-10-30T22:18:25+00:00" + "time": "2018-04-29T16:43:54+00:00" }, { "name": "matthiasmullie/minify", @@ -1203,16 +1214,16 @@ }, { "name": "rockettheme/toolbox", - "version": "1.4.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/rockettheme/toolbox.git", - "reference": "ca12fcc980dc7bdf3f955beb74278a070448a485" + "reference": "93f5c3d5e173cee7419df20eed52711471abbc3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/ca12fcc980dc7bdf3f955beb74278a070448a485", - "reference": "ca12fcc980dc7bdf3f955beb74278a070448a485", + "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/93f5c3d5e173cee7419df20eed52711471abbc3e", + "reference": "93f5c3d5e173cee7419df20eed52711471abbc3e", "shasum": "" }, "require": { @@ -1248,7 +1259,7 @@ "php", "rockettheme" ], - "time": "2018-06-13T17:11:52+00:00" + "time": "2018-08-08T18:03:32+00:00" }, { "name": "seld/cli-prompt", @@ -1300,37 +1311,45 @@ }, { "name": "symfony/console", - "version": "v2.8.41", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7" + "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e8e59b74ad1274714dad2748349b55e3e6e630c7", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7", + "url": "https://api.github.com/repos/symfony/console/zipball/6b217594552b9323bcdcfc14f8a0ce126e84cd73", + "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/debug": "^2.7.2|~3.0.0", + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1|~3.0.0", - "symfony/process": "~2.1|~3.0.0" + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" }, "suggest": { "psr/log-implementation": "For using the console logger", "symfony/event-dispatcher": "", + "symfony/lock": "", "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1357,37 +1376,36 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/debug", - "version": "v3.0.9", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" + "reference": "d5a058ff6ecad26b30c1ba452241306ea34c65cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", + "url": "https://api.github.com/repos/symfony/debug/zipball/d5a058ff6ecad26b30c1ba452241306ea34c65cc", + "reference": "d5a058ff6ecad26b30c1ba452241306ea34c65cc", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "psr/log": "~1.0" }, "conflict": { "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" }, "require-dev": { - "symfony/class-loader": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0" + "symfony/http-kernel": "~2.8|~3.0|~4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1414,31 +1432,34 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-07-30T07:22:48+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.41", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c" + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^2.0.5|~3.0.0", - "symfony/dependency-injection": "~2.6|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/stopwatch": "~2.3|~3.0.0" + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0" }, "suggest": { "symfony/dependency-injection": "", @@ -1447,7 +1468,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1474,29 +1495,32 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-04-06T07:35:03+00:00" + "time": "2018-07-26T09:06:28+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-ctype": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -1529,20 +1553,20 @@ "polyfill", "portable" ], - "time": "2018-04-30T19:57:29+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "7cb8436a814d5b0fcf292810ee26f8b0cb47584d" + "reference": "bcc0cd69185b8a5d8b4a5400c489ed3333bf9bb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/7cb8436a814d5b0fcf292810ee26f8b0cb47584d", - "reference": "7cb8436a814d5b0fcf292810ee26f8b0cb47584d", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/bcc0cd69185b8a5d8b4a5400c489ed3333bf9bb2", + "reference": "bcc0cd69185b8a5d8b4a5400c489ed3333bf9bb2", "shasum": "" }, "require": { @@ -1554,7 +1578,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -1588,20 +1612,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", "shasum": "" }, "require": { @@ -1613,7 +1637,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -1647,24 +1671,24 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/var-dumper", - "version": "v2.8.41", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c3d7a096ccaba86e2fadeb444ca32fcbc5a31ebd" + "reference": "f62a394bd3de96f2f5e8f4c7d685035897fb3cb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c3d7a096ccaba86e2fadeb444ca32fcbc5a31ebd", - "reference": "c3d7a096ccaba86e2fadeb444ca32fcbc5a31ebd", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f62a394bd3de96f2f5e8f4c7d685035897fb3cb3", + "reference": "f62a394bd3de96f2f5e8f4c7d685035897fb3cb3", "shasum": "" }, "require": { - "php": ">=5.3.9", + "php": "^5.5.9|>=7.0.8", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -1676,12 +1700,13 @@ }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", "ext-symfony_debug": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1715,30 +1740,39 @@ "debug", "dump" ], - "time": "2018-04-25T14:40:02+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/yaml", - "version": "v2.8.41", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff" + "reference": "810af2d35fc72b6cf5c01116806d2b65ccaaf2e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff", - "reference": "51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff", + "url": "https://api.github.com/repos/symfony/yaml/zipball/810af2d35fc72b6cf5c01116806d2b65ccaaf2e2", + "reference": "810af2d35fc72b6cf5c01116806d2b65ccaaf2e2", "shasum": "" }, "require": { - "php": ">=5.3.9", + "php": "^5.5.9|>=7.0.8", "symfony/polyfill-ctype": "~1.8" }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1765,24 +1799,25 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-05-01T22:52:40+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "twig/twig", - "version": "v1.35.3", + "version": "v1.35.4", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f" + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "psr/container": "^1.0", @@ -1821,16 +1856,16 @@ }, { "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "keywords": [ "templating" ], - "time": "2018-03-20T04:25:58+00:00" + "time": "2018-07-13T07:12:17+00:00" } ], "packages-dev": [ @@ -1895,28 +1930,28 @@ }, { "name": "codeception/codeception", - "version": "2.4.1", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "bca3547632556875f1cdd567d6057cc14fe472b8" + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/bca3547632556875f1cdd567d6057cc14fe472b8", - "reference": "bca3547632556875f1cdd567d6057cc14fe472b8", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", "shasum": "" }, "require": { "behat/gherkin": "^4.4.0", "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", - "codeception/stub": "^1.0", + "codeception/stub": "^2.0", "ext-json": "*", "ext-mbstring": "*", "facebook/webdriver": ">=1.1.3 <2.0", "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", + "php": ">=5.6.0 <8.0", "symfony/browser-kit": ">=2.7 <5.0", "symfony/console": ">=2.7 <5.0", "symfony/css-selector": ">=2.7 <5.0", @@ -1982,20 +2017,20 @@ "functional testing", "unit testing" ], - "time": "2018-03-31T22:30:43+00:00" + "time": "2018-08-01T07:21:49+00:00" }, { "name": "codeception/phpunit-wrapper", - "version": "6.0.9", + "version": "6.0.10", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "450f1cfc5f49539c421061e64338f5edb8baad6a" + "reference": "7057e599d97b02b4efb009681a43b327dbce138a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/450f1cfc5f49539c421061e64338f5edb8baad6a", - "reference": "450f1cfc5f49539c421061e64338f5edb8baad6a", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7057e599d97b02b4efb009681a43b327dbce138a", + "reference": "7057e599d97b02b4efb009681a43b327dbce138a", "shasum": "" }, "require": { @@ -2028,26 +2063,23 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2018-03-31T18:50:01+00:00" + "time": "2018-06-20T20:08:14+00:00" }, { "name": "codeception/stub", - "version": "1.0.4", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" + "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", + "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", "shasum": "" }, "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" - }, - "require-dev": { "phpunit/phpunit": ">=4.8 <8.0" }, "type": "library", @@ -2061,7 +2093,7 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" + "time": "2018-07-26T11:55:37+00:00" }, { "name": "doctrine/instantiator", @@ -2119,30 +2151,38 @@ }, { "name": "facebook/webdriver", - "version": "1.4.1", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/facebook/php-webdriver.git", - "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83" + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83", - "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bd8c740097eb9f2fc3735250fc1912bc811a954e", + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e", "shasum": "" }, "require": { "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", "ext-zip": "*", - "php": "^5.5 || ~7.0", - "symfony/process": "^2.8 || ^3.1" + "php": "^5.6 || ~7.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "php-coveralls/php-coveralls": "^2.0", "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "4.6.* || ~5.0", - "satooshi/php-coveralls": "^1.0", - "squizlabs/php_codesniffer": "^2.6" + "phpunit/phpunit": "^5.7", + "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", + "squizlabs/php_codesniffer": "^2.6", + "symfony/var-dumper": "^3.3 || ^4.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" }, "type": "library", "extra": { @@ -2167,20 +2207,20 @@ "selenium", "webdriver" ], - "time": "2017-04-28T14:54:49+00:00" + "time": "2018-05-16T17:37:13+00:00" }, { "name": "fzaninotto/faker", - "version": "v1.7.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de", + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de", "shasum": "" }, "require": { @@ -2188,7 +2228,7 @@ }, "require-dev": { "ext-intl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.7", "squizlabs/php_codesniffer": "^1.5" }, "type": "library", @@ -2217,7 +2257,7 @@ "faker", "fixtures" ], - "time": "2017-08-15T16:48:10+00:00" + "time": "2018-07-12T10:23:15+00:00" }, { "name": "guzzlehttp/guzzle", @@ -2391,22 +2431,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", + "php": "^5.6 || ^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -2432,20 +2472,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" + "time": "2017-11-10T14:09:06+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { @@ -2479,20 +2519,20 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-06-03T08:32:36+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -2504,12 +2544,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -2542,7 +2582,7 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3294,16 +3334,16 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.11", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79" + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6668d1a6182d5a8dec65a1c863a4c1d963816c0", + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0", "shasum": "" }, "require": { @@ -3347,20 +3387,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" + "time": "2018-07-26T09:06:28+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.11", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "d2ce52290b648ae33b5301d09bc14ee378612914" + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/d2ce52290b648ae33b5301d09bc14ee378612914", - "reference": "d2ce52290b648ae33b5301d09bc14ee378612914", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/edda5a6155000ff8c3a3f85ee5c421af93cca416", + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416", "shasum": "" }, "require": { @@ -3400,20 +3440,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-05-16T12:49:49+00:00" + "time": "2018-07-26T09:06:28+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.11", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8" + "reference": "452bfc854b60134438e3824b159b0d24a5892331" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8", - "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/452bfc854b60134438e3824b159b0d24a5892331", + "reference": "452bfc854b60134438e3824b159b0d24a5892331", "shasum": "" }, "require": { @@ -3457,20 +3497,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-05-01T22:53:27+00:00" + "time": "2018-07-26T10:03:52+00:00" }, { "name": "symfony/finder", - "version": "v3.4.11", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "472a92f3df8b247b49ae364275fb32943b9656c6" + "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/472a92f3df8b247b49ae364275fb32943b9656c6", - "reference": "472a92f3df8b247b49ae364275fb32943b9656c6", + "url": "https://api.github.com/repos/symfony/finder/zipball/8a84fcb207451df0013b2c74cbbf1b62d47b999a", + "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a", "shasum": "" }, "require": { @@ -3506,20 +3546,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/process", - "version": "v3.4.11", + "version": "v3.4.14", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4cbf2db9abcb01486a21b7a059e03a62fae63187" + "reference": "0414db29bd770ec5a4152683e655f55efd4fa60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4cbf2db9abcb01486a21b7a059e03a62fae63187", - "reference": "4cbf2db9abcb01486a21b7a059e03a62fae63187", + "url": "https://api.github.com/repos/symfony/process/zipball/0414db29bd770ec5a4152683e655f55efd4fa60f", + "reference": "0414db29bd770ec5a4152683e655f55efd4fa60f", "shasum": "" }, "require": { @@ -3555,7 +3595,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "victorjonsson/markdowndocs", @@ -3662,7 +3702,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.5.9", + "php": ">=5.6.4", "ext-mbstring": "*", "ext-openssl": "*", "ext-curl": "*", @@ -3670,6 +3710,6 @@ }, "platform-dev": [], "platform-overrides": { - "php": "5.5.9" + "php": "5.6.4" } } diff --git a/index.php b/index.php index 0082125034..db077f5eb6 100644 --- a/index.php +++ b/index.php @@ -7,7 +7,7 @@ */ namespace Grav; -define('GRAV_PHP_MIN', '5.5.9'); +define('GRAV_PHP_MIN', '5.6.4'); // Ensure vendor libraries exist $autoload = __DIR__ . '/vendor/autoload.php'; @@ -15,7 +15,7 @@ die("Please run: bin/grav install"); } -if (PHP_SAPI == 'cli-server') { +if (PHP_SAPI === 'cli-server') { if (!isset($_SERVER['PHP_CLI_ROUTER'])) { die("PHP webserver requires a router to run Grav, please use:
php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php
"); } diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 53f8ba179e..5bc2e49706 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -996,6 +996,18 @@ form: validate: type: bool + session.initialize: + type: toggle + label: PLUGIN_ADMIN.SESSION_INITIALIZE + help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + session.timeout: type: text size: small @@ -1206,3 +1218,27 @@ form: placeholder: "e.g. http://yoursite.com/yourpath" label: PLUGIN_ADMIN.CUSTOM_BASE_URL help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP + + strict_mode.yaml_compat: + type: toggle + label: PLUGIN_ADMIN.STRICT_YAML_COMPAT + highlight: 1 + default: 1 + help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + strict_mode.twig_compat: + type: toggle + label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT + highlight: 1 + default: 1 + help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index 7717a01bb8..ac7717b3e3 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -94,6 +94,7 @@ form: twofa_secret: type: 2fa_secret outerclasses: 'twofa-secret' + markdown: true label: PLUGIN_ADMIN.2FA_SECRET sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP diff --git a/system/config/system.yaml b/system/config/system.yaml index c05ee4abf7..065e059f20 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -88,7 +88,7 @@ twig: cache: true # Set to true to enable Twig caching debug: true # Enable Twig debug auto_reload: true # Refresh cache on changes - autoescape: false # Autoescape Twig vars + autoescape: false # Autoescape Twig vars (DEPRECATED, always enabled in strict mode) undefined_functions: true # Allow undefined functions undefined_filters: true # Allow undefined filters umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 @@ -146,3 +146,7 @@ gpm: method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help. official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security + +strict_mode: + yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility + twig_compat: true # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false) diff --git a/system/defines.php b/system/defines.php index 28c4434f09..fe02c9e72c 100644 --- a/system/defines.php +++ b/system/defines.php @@ -8,12 +8,12 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.4.6'); +define('GRAV_VERSION', '1.5.0'); define('GRAV_TESTING', false); define('DS', '/'); if (!defined('GRAV_PHP_MIN')) { - define('GRAV_PHP_MIN', '5.5.9'); + define('GRAV_PHP_MIN', '5.6.4'); } // Directories and Paths diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php index 7980671754..83b13ec350 100644 --- a/system/src/Grav/Common/Config/ConfigFileFinder.php +++ b/system/src/Grav/Common/Config/ConfigFileFinder.php @@ -207,7 +207,7 @@ protected function detectInFolder($folder, $lookup = null) continue; } - $name = $directory->getBasename(); + $name = $directory->getFilename(); $find = ($lookup ?: $name) . '.yaml'; $filename = "{$path}/{$name}/{$find}"; diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php index e9180fea8e..fba7fd1031 100644 --- a/system/src/Grav/Common/Config/Setup.php +++ b/system/src/Grav/Common/Config/Setup.php @@ -262,18 +262,22 @@ protected function check(UniformResourceLocator $locator) ); } - if (!$locator->findResource('environment://config', true)) { - // If environment does not have its own directory, remove it from the lookup. - $this->set('streams.schemes.environment.prefixes', ['config' => []]); - $this->initializeLocator($locator); - } + try { + if (!$locator->findResource('environment://config', true)) { + // If environment does not have its own directory, remove it from the lookup. + $this->set('streams.schemes.environment.prefixes', ['config' => []]); + $this->initializeLocator($locator); + } - // Create security.yaml if it doesn't exist. - $filename = $locator->findResource('config://security.yaml', true, true); - $file = YamlFile::instance($filename); - if (!$file->exists()) { - $file->save(['salt' => Utils::generateRandomString(14)]); - $file->free(); + // Create security.yaml if it doesn't exist. + $filename = $locator->findResource('config://security.yaml', true, true); + $file = YamlFile::instance($filename); + if (!$file->exists()) { + $file->save(['salt' => Utils::generateRandomString(14)]); + $file->free(); + } + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e); } } } diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 8ab5cf1854..3d982b7498 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -11,8 +11,8 @@ use Grav\Common\Grav; use Grav\Common\Utils; use Symfony\Component\Yaml\Exception\ParseException; -use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Yaml; +use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYaml; class Validation { @@ -107,7 +107,7 @@ public static function filter($value, array $field) $method = 'filter' . ucfirst(strtr($type, '-', '_')); // If this is a YAML field validate/filter as such - if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { + if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { $method = 'filterYaml'; } @@ -128,10 +128,12 @@ public static function filter($value, array $field) */ public static function typeText($value, array $params, array $field) { - if (!is_string($value)) { + if (!is_string($value) && !is_numeric($value)) { return false; } + $value = (string)$value; + if (isset($params['min']) && strlen($value) < $params['min']) { return false; } @@ -643,13 +645,21 @@ protected static function filterList($value, array $params, array $field) public static function filterYaml($value, $params) { + if (!is_string($value)) { + return $value; + } + try { - if (is_string($value)) { - return (array) Yaml::parse($value); - } else { - return $value; - } + return (array) Yaml::parse($value); } catch (ParseException $e) { + // If YAML compatibility mode is set on, fall back to older YAML parser. + if (Grav::instance()['config']->get('system.strict_mode.yaml_compat', true)) { + try { + return (array) FallbackYaml::parse($value); + } catch (ParseException $e2) { + } + } + return $value; } } diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php index 4687325e12..d687280340 100644 --- a/system/src/Grav/Common/Debugger.php +++ b/system/src/Grav/Common/Debugger.php @@ -58,8 +58,15 @@ public function init() $this->enabled = $this->config->get('system.debugger.enabled'); if ($this->enabled()) { + + $plugins_config = (array)$this->config->get('plugins'); + + ksort($plugins_config); + + $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config')); - $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('plugins'), 'Plugins')); + $this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins')); + $this->addMessage('Grav v' . GRAV_VERSION); } return $this; diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php index a0e230f143..4da55d9eb1 100644 --- a/system/src/Grav/Common/File/CompiledFile.php +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -20,9 +20,6 @@ trait CompiledFile */ public function content($var = null) { - // Set some options - $this->settings(['native' => true, 'compat' => true]); - try { // If nothing has been loaded, attempt to get pre-compiled version of the file first. if ($var === null && $this->raw === null && $this->content === null) { diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index a7cbee402b..696ba657cd 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -13,7 +13,7 @@ use Grav\Common\Inflector; use Grav\Common\Iterator; use Grav\Common\Utils; -use Symfony\Component\Yaml\Yaml; +use RocketTheme\Toolbox\File\YamlFile; class GPM extends Iterator { @@ -624,7 +624,10 @@ public static function getBlueprints($source) return false; } - $blueprint = (array)Yaml::parse(file_get_contents($blueprint_file)); + $file = YamlFile::instance($blueprint_file); + $blueprint = (array)$file->content(); + $file->free(); + return $blueprint; } @@ -873,7 +876,9 @@ public function getDependencies($packages) // get currently installed version $locator = Grav::instance()['locator']; $blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml'); - $package_yaml = Yaml::parse(file_get_contents($blueprints_path)); + $file = YamlFile::instance($blueprints_path); + $package_yaml = $file->content(); + $file->free(); $currentlyInstalledVersion = $package_yaml['version']; // if requirement is next significant release, check is compatible with currently installed version, might not be diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index 6c385b8d82..f16cdcca84 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -296,17 +296,17 @@ public static function sophisticatedInstall($source_path, $install_path, $ignore { foreach (new \DirectoryIterator($source_path) as $file) { - if ($file->isLink() || $file->isDot() || in_array($file->getBasename(),$ignores)) { + if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores)) { continue; } - $path = $install_path . DS . $file->getBasename(); + $path = $install_path . DS . $file->getFilename(); if ($file->isDir()) { Folder::delete($path); Folder::move($file->getPathname(), $path); - if ($file->getBasename() == 'bin') { + if ($file->getFilename() === 'bin') { foreach (glob($path . DS . '*') as $bin_file) { @chmod($bin_file, 0755); } diff --git a/system/src/Grav/Common/GPM/Licenses.php b/system/src/Grav/Common/GPM/Licenses.php index fed5b642eb..6ca596a031 100644 --- a/system/src/Grav/Common/GPM/Licenses.php +++ b/system/src/Grav/Common/GPM/Licenses.php @@ -114,7 +114,7 @@ public static function getLicenseFile() { if (!isset(self::$file)) { - $path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';; + $path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml'; if (!file_exists($path)) { touch($path); } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index e6a49a1656..84260d01c0 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -9,7 +9,6 @@ namespace Grav\Common; use Grav\Common\Config\Config; -use Grav\Common\Language\Language; use Grav\Common\Page\Medium\ImageMedium; use Grav\Common\Page\Medium\Medium; use Grav\Common\Page\Page; @@ -205,11 +204,8 @@ public function redirect($route, $code = null) */ public function redirectLangSafe($route, $code = null) { - /** @var Language $language */ - $language = $this['language']; - - if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) { - $this->redirect($language->getLanguage() . $route, $code); + if (!$this['uri']->isExternal($route)) { + $this->redirect($this['pages']->route($route), $code); } else { $this->redirect($route, $code); } @@ -443,7 +439,7 @@ public function fallbackUrl($path) /** @var Config $config */ $config = $this['config']; - $uri_extension = $uri->extension(); + $uri_extension = strtolower($uri->extension()); $fallback_types = $config->get('system.media.allowed_fallback_types', null); $supported_types = $config->get('media.types'); diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php index f4a17ba7c5..61c2338095 100644 --- a/system/src/Grav/Common/Inflector.php +++ b/system/src/Grav/Common/Inflector.php @@ -190,10 +190,11 @@ public function underscorize($word) public function hyphenize($word) { $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word); - $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1-\2', $regex1); - $regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex2); + $regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1); + $regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2); + $regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3); - return strtolower($regex3); + return strtolower($regex4); } /** diff --git a/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php new file mode 100644 index 0000000000..06f71a5ece --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php @@ -0,0 +1,9 @@ +getMediaFolder(); + + if (strpos($folder, '://')) { + return $folder; + } + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $user = $locator->findResource('user://'); + if (strpos($folder, $user) === 0) { + return 'user://' . substr($folder, strlen($user)+1); + } + + return null; + } + + /** + * Gets the associated media collection. + * + * @return MediaCollectionInterface Representation of associated media. + */ + public function getMedia() + { + $cache = $this->getMediaCache(); + + if ($this->media === null) { + // Use cached media if possible. + $cacheKey = md5('media' . $this->getCacheKey()); + if (!$media = $cache->fetch($cacheKey)) { + $media = new Media($this->getMediaFolder(), $this->getMediaOrder()); + $cache->save($cacheKey, $media); + } + $this->media = $media; + } + + return $this->media; + } + + /** + * Sets the associated media collection. + * + * @param MediaCollectionInterface $media Representation of associated media. + * @return $this + */ + protected function setMedia(MediaCollectionInterface $media) + { + $cache = $this->getMediaCache(); + $cacheKey = md5('media' . $this->getCacheKey()); + $cache->save($cacheKey, $media); + + $this->media = $media; + + return $this; + } + + /** + * Clear media cache. + */ + protected function clearMediaCache() + { + $cache = $this->getMediaCache(); + $cacheKey = md5('media' . $this->getCacheKey()); + $cache->delete($cacheKey); + } + + /** + * @return Cache + */ + protected function getMediaCache() + { + return Grav::instance()['cache']; + } + + /** + * @return string + */ + abstract protected function getCacheKey(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageInterface.php b/system/src/Grav/Common/Page/Interfaces/PageInterface.php new file mode 100644 index 0000000000..14d6cdc1e5 --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageInterface.php @@ -0,0 +1,9 @@ +path = $path; + $this->media_order = $media_order; $this->__wakeup(); $this->init(); @@ -86,7 +88,7 @@ protected function init() /** @var \DirectoryIterator $info */ foreach ($iterator as $path => $info) { // Ignore folders and Markdown files. - if (!$info->isFile() || $info->getExtension() === 'md' || $info->getBasename()[0] === '.') { + if (!$info->isFile() || $info->getExtension() === 'md' || $info->getFilename()[0] === '.') { continue; } diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index 4c95938c30..58f9886101 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -10,9 +10,11 @@ use Grav\Common\Getters; use Grav\Common\Grav; +use Grav\Common\Media\Interfaces\MediaCollectionInterface; +use Grav\Common\Media\Interfaces\MediaObjectInterface; use Grav\Common\Utils; -abstract class AbstractMedia extends Getters +abstract class AbstractMedia extends Getters implements MediaCollectionInterface { protected $gettersVariable = 'instances'; @@ -21,6 +23,7 @@ abstract class AbstractMedia extends Getters protected $videos = []; protected $audios = []; protected $files = []; + protected $media_order; /** * Get medium by filename. @@ -62,7 +65,7 @@ public function offsetGet($offset) /** * Get a list of all media. * - * @return array|Medium[] + * @return array|MediaObjectInterface[] */ public function all() { @@ -74,7 +77,7 @@ public function all() /** * Get a list of all image media. * - * @return array|Medium[] + * @return array|MediaObjectInterface[] */ public function images() { @@ -85,7 +88,7 @@ public function images() /** * Get a list of all video media. * - * @return array|Medium[] + * @return array|MediaObjectInterface[] */ public function videos() { @@ -96,7 +99,7 @@ public function videos() /** * Get a list of all audio media. * - * @return array|Medium[] + * @return array|MediaObjectInterface[] */ public function audios() { @@ -107,7 +110,7 @@ public function audios() /** * Get a list of all file media. * - * @return array|Medium[] + * @return array|MediaObjectInterface[] */ public function files() { @@ -117,7 +120,7 @@ public function files() /** * @param string $name - * @param Medium $file + * @param MediaObjectInterface $file */ protected function add($name, $file) { @@ -145,14 +148,20 @@ protected function add($name, $file) */ protected function orderMedia($media) { - $page = Grav::instance()['pages']->get($this->path); + if (null === $this->media_order) { + $page = Grav::instance()['pages']->get($this->path); - if ($page && isset($page->header()->media_order)) { - $media_order = array_map('trim', explode(',', $page->header()->media_order)); - $media = Utils::sortArrayByArray($media, $media_order); + if ($page && isset($page->header()->media_order)) { + $this->media_order = array_map('trim', explode(',', $page->header()->media_order)); + } + } + + if (!empty($this->media_order) && is_array($this->media_order)) { + $media = Utils::sortArrayByArray($media, $this->media_order); } else { ksort($media, SORT_NATURAL | SORT_FLAG_CASE); } + return $media; } diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 2851225dd1..2f1f176a62 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -11,6 +11,7 @@ use Grav\Common\Data\Blueprint; use Grav\Common\Grav; use Grav\Common\Utils; +use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; class ImageMedium extends Medium { @@ -164,12 +165,18 @@ public function path($reset = true) */ public function url($reset = true) { - $image_path = Grav::instance()['locator']->findResource('cache://images', true); - $image_dir = Grav::instance()['locator']->findResource('cache://images', false); + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $image_path = $locator->findResource('cache://images', true); + $image_dir = $locator->findResource('cache://images', false); $saved_image_path = $this->saveImage(); $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path); + if ($locator->isStream($output)) { + $output = $locator->findResource($output, false); + } + if (Utils::startsWith($output, $image_path)) { $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output); } diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php index 36b893c4bd..0a4b9bf5f8 100644 --- a/system/src/Grav/Common/Page/Medium/Medium.php +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -12,9 +12,9 @@ use Grav\Common\Grav; use Grav\Common\Data\Data; use Grav\Common\Data\Blueprint; -use Grav\Common\Utils; +use Grav\Common\Media\Interfaces\MediaObjectInterface; -class Medium extends Data implements RenderableInterface +class Medium extends Data implements RenderableInterface, MediaObjectInterface { use ParsedownHtmlTrait; @@ -199,7 +199,12 @@ public function relativePath($reset = true) */ public function url($reset = true) { - $output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $this->get('filepath')); + $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath')); + + $locator = Grav::instance()['locator']; + if ($locator->isStream($output)) { + $output = $locator->findResource($output, false); + } if ($reset) { $this->reset(); diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php index b3c8eed852..23271bf35d 100644 --- a/system/src/Grav/Common/Page/Medium/VideoMedium.php +++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php @@ -94,6 +94,40 @@ public function autoplay($status = false) return $this; } + /** + * Allows to set the playsinline attribute + * + * @param bool $status + * @return $this + */ + public function playsinline($status = false) + { + if($status) { + $this->attributes['playsinline'] = true; + } else { + unset($this->attributes['playsinline']); + } + + return $this; + } + + /** + * Allows to set the muted attribute + * + * @param bool $status + * @return $this + */ + public function muted($status = false) + { + if($status) { + $this->attributes['muted'] = true; + } else { + unset($this->attributes['muted']); + } + + return $this; + } + /** * Reset medium. * diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index e92e03bffb..b1c8b9775f 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -12,11 +12,14 @@ use Grav\Common\Cache; use Grav\Common\Config\Config; use Grav\Common\Data\Blueprint; +use Grav\Common\File\CompiledYamlFile; use Grav\Common\Filesystem\Folder; use Grav\Common\Grav; use Grav\Common\Language\Language; use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\ParsedownExtra; +use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Media\Traits\MediaTrait; use Grav\Common\Taxonomy; use Grav\Common\Uri; use Grav\Common\Utils; @@ -27,8 +30,10 @@ define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u'); -class Page +class Page implements PageInterface { + use MediaTrait; + /** * @var string Filename. Leave as null if page is folder. */ @@ -65,7 +70,6 @@ class Page protected $summary; protected $raw_content; protected $pagination; - protected $media; protected $metadata; protected $title; protected $max_count; @@ -318,8 +322,6 @@ public function header($var = null) if (!$this->header) { $file = $this->file(); if ($file) { - // Set some options - $file->settings(['native' => true, 'compat' => true]); try { $this->raw_content = $file->markdown(); $this->frontmatter = $file->frontmatter(); @@ -328,11 +330,12 @@ public function header($var = null) if (!Utils::isAdminPlugin()) { // If there's a `frontmatter.yaml` file merge that in with the page header // note page's own frontmatter has precedence and will overwrite any defaults - $frontmatter_file = $this->path . '/' . $this->folder . '/frontmatter.yaml'; - if (file_exists($frontmatter_file)) { - $frontmatter_data = (array)Yaml::parse(file_get_contents($frontmatter_file)); + $frontmatterFile = CompiledYamlFile::instance($this->path . '/' . $this->folder . '/frontmatter.yaml'); + if ($frontmatterFile->exists()) { + $frontmatter_data = (array)$frontmatterFile->content(); $this->header = (object)array_replace_recursive($frontmatter_data, (array)$this->header); + $frontmatterFile->free(); } // Process frontmatter with Twig if enabled if (Grav::instance()['config']->get('system.pages.frontmatter.process_twig') === true) { @@ -813,6 +816,8 @@ public function getRawContent() */ public function setRawContent($content) { + $content = $content === null ? '': $content; + $this->content = $content; } @@ -1122,6 +1127,14 @@ public function toJson() return json_encode($this->toArray()); } + /** + * @return string + */ + protected function getCacheKey() + { + return $this->id(); + } + /** * Gets and sets the associated media as found in the page folder. * @@ -1131,23 +1144,33 @@ public function toJson() */ public function media($var = null) { - /** @var Cache $cache */ - $cache = Grav::instance()['cache']; - if ($var) { - $this->media = $var; - } - if ($this->media === null) { - // Use cached media if possible. - $media_cache_id = md5('media' . $this->id()); - if (!$media = $cache->fetch($media_cache_id)) { - $media = new Media($this->path()); - $cache->save($media_cache_id, $media); - } - $this->media = $media; + $this->setMedia($var); } - return $this->media; + return $this->getMedia(); + } + + /** + * Get filesystem path to the associated media. + * + * @return string|null + */ + public function getMediaFolder() + { + return $this->path(); + } + + /** + * Get display order for the associated media. + * + * @return array Empty array means default ordering. + */ + public function getMediaOrder() + { + $header = $this->header(); + + return isset($header->media_order) ? array_map('trim', explode(',', (string)$header->media_order)) : []; } /** @@ -1626,14 +1649,19 @@ public function canonical($include_lang = true) * Gets the url for the Page. * * @param bool $include_host Defaults false, but true would include http://yourhost.com - * @param bool $canonical true to return the canonical URL - * @param bool $include_lang + * @param bool $canonical True to return the canonical URL + * @param bool $include_base Include base url on multisite as well as language code * @param bool $raw_route * * @return string The url. */ - public function url($include_host = false, $canonical = false, $include_lang = true, $raw_route = false) + public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false) { + // Override any URL when external_url is set + if (isset($this->external_url)) { + return $this->external_url; + } + $grav = Grav::instance(); /** @var Pages $pages */ @@ -1642,41 +1670,25 @@ public function url($include_host = false, $canonical = false, $include_lang = t /** @var Config $config */ $config = $grav['config']; - /** @var Language $language */ - $language = $grav['language']; - - /** @var Uri $uri */ - $uri = $grav['uri']; - - // Override any URL when external_url is set - if (isset($this->external_url)) { - return $this->external_url; - } - - // get pre-route - if ($include_lang && $language->enabled()) { - $pre_route = $language->getLanguageURLPrefix(); - } else { - $pre_route = ''; - } + // get base route (multisite base and language) + $route = $include_base ? $pages->baseRoute() : ''; // add full route if configured to do so - if ($config->get('system.absolute_urls', false)) { + if (!$include_host && $config->get('system.absolute_urls', false)) { $include_host = true; } - // get canonical route if requested if ($canonical) { - $route = $pre_route . $this->routeCanonical(); + $route .= $this->routeCanonical(); } elseif ($raw_route) { - $route = $pre_route . $this->rawRoute(); + $route .= $this->rawRoute(); } else { - $route = $pre_route . $this->route(); + $route .= $this->route(); } - $rootUrl = $uri->rootUrl($include_host) . $pages->base(); - - $url = $rootUrl . '/' . trim($route, '/') . $this->urlExtension(); + /** @var Uri $uri */ + $uri = $grav['uri']; + $url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension(); // trim trailing / if not root if ($url !== '/') { @@ -1790,7 +1802,7 @@ public function routeAliases($var = null) public function routeCanonical($var = null) { if ($var !== null) { - $this->routes['canonical'] = (array)$var; + $this->routes['canonical'] = $var; } if (!empty($this->routes) && isset($this->routes['canonical'])) { diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 12afa7e88d..ac0e41cb02 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -49,7 +49,7 @@ class Pages /** * @var array|string[] */ - protected $baseUrl = []; + protected $baseRoute = []; /** * @var array|string[] @@ -120,7 +120,7 @@ public function base($path = null) if ($path !== null) { $path = trim($path, '/'); $this->base = $path ? '/' . $path : null; - $this->baseUrl = []; + $this->baseRoute = []; } return $this->base; @@ -128,39 +128,61 @@ public function base($path = null) /** * - * Get base URL for Grav pages. + * Get base route for Grav pages. * - * @param string $lang Optional language code for multilingual links. - * @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default. + * @param string $lang Optional language code for multilingual routes. * * @return string */ - public function baseUrl($lang = null, $absolute = null) + public function baseRoute($lang = null) { - $lang = (string) $lang; - $type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative'); - $key = "{$lang} {$type}"; - - if (!isset($this->baseUrl[$key])) { - /** @var Config $config */ - $config = $this->grav['config']; + $key = $lang ?: 'default'; + if (!isset($this->baseRoute[$key])) { /** @var Language $language */ $language = $this->grav['language']; - if (!$lang) { - $lang = $language->getActive(); - } + $path_base = rtrim($this->base(), '/'); + $path_lang = $language->enabled() ? $language->getLanguageURLPrefix($lang) : ''; - $path_append = rtrim($this->grav['pages']->base(), '/'); - if ($language->getDefault() !== $lang || $config->get('system.languages.include_default_lang') === true) { - $path_append .= $lang ? '/' . $lang : ''; - } + $this->baseRoute[$key] = $path_base . $path_lang; + } + + return $this->baseRoute[$key]; + } - $this->baseUrl[$key] = $this->grav[$type] . $path_append; + /** + * + * Get route for Grav site. + * + * @param string $route Optional route to the page. + * @param string $lang Optional language code for multilingual links. + * + * @return string + */ + public function route($route = '/', $lang = null) + { + if (!$route || $route === '/') { + return $this->baseRoute($lang) ?: '/'; } - return $this->baseUrl[$key]; + return $this->baseRoute($lang) . $route; + } + + /** + * + * Get base URL for Grav pages. + * + * @param string $lang Optional language code for multilingual links. + * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default. + * + * @return string + */ + public function baseUrl($lang = null, $absolute = null) + { + $type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative'); + + return $this->grav[$type] . $this->baseRoute($lang); } /** @@ -179,7 +201,7 @@ public function homeUrl($lang = null, $absolute = null) /** * - * Get home URL for Grav site. + * Get URL for Grav site. * * @param string $route Optional route to the page. * @param string $lang Optional language code for multilingual links. @@ -189,7 +211,7 @@ public function homeUrl($lang = null, $absolute = null) */ public function url($route = '/', $lang = null, $absolute = null) { - if ($route === '/') { + if (!$route || $route === '/') { return $this->homeUrl($lang, $absolute); } @@ -1041,7 +1063,7 @@ function ($str) { } // Ignore all files in ignore list. - if (\in_array($file->getBasename(), $this->ignore_files, true)) { + if (\in_array($filename, $this->ignore_files, true)) { continue; } diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php index 5e407a905d..2a9d523811 100644 --- a/system/src/Grav/Common/Plugins.php +++ b/system/src/Grav/Common/Plugins.php @@ -33,7 +33,7 @@ public function __construct() if (!$directory->isDir()) { continue; } - $plugins[] = $directory->getBasename(); + $plugins[] = $directory->getFilename(); } natsort($plugins); diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php index e3f25691c9..96e6a7e805 100644 --- a/system/src/Grav/Common/Processors/InitializeProcessor.php +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -8,6 +8,10 @@ namespace Grav\Common\Processors; +use Grav\Common\Config\Config; +use Grav\Common\Uri; +use Grav\Common\Utils; + class InitializeProcessor extends ProcessorBase implements ProcessorInterface { public $id = 'init'; @@ -15,29 +19,36 @@ class InitializeProcessor extends ProcessorBase implements ProcessorInterface public function process() { - $this->container['config']->debug(); + /** @var Config $config */ + $config = $this->container['config']; + $config->debug(); // Use output buffering to prevent headers from being sent too early. ob_start(); - if ($this->container['config']->get('system.cache.gzip')) { + if ($config->get('system.cache.gzip') && !@ob_start('ob_gzhandler')) { // Enable zip/deflate with a fallback in case of if browser does not support compressing. - if (!@ob_start("ob_gzhandler")) { - ob_start(); - } + ob_start(); } // Initialize the timezone. - if ($this->container['config']->get('system.timezone')) { + if ($config->get('system.timezone')) { date_default_timezone_set($this->container['config']->get('system.timezone')); } // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS. - if ($this->container['config']->get('system.session.initialize', 1) && isset($this->container['session'])) { + if (isset($this->container['session']) && $config->get('system.session.initialize', true)) { $this->container['session']->init(); } - // Initialize uri. - $this->container['uri']->init(); + /** @var Uri $uri */ + $uri = $this->container['uri']; + $uri->init(); + + // Redirect pages with trailing slash if configured to do so. + $path = $uri->path() ?: '/'; + if ($path !== '/' && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($path, '/')) { + $this->container->redirect(rtrim($path, '/'), 302); + } $this->container->setLocale(); } diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index 25c1a126ee..379c042e72 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -16,6 +16,7 @@ use Grav\Common\Config\Setup; use Pimple\Container; use Pimple\ServiceProviderInterface; +use RocketTheme\Toolbox\File\YamlFile; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; class ConfigServiceProvider implements ServiceProviderInterface @@ -31,7 +32,14 @@ public function register(Container $container) }; $container['config'] = function ($c) { - return static::load($c); + $config = static::load($c); + + // After configuration has been loaded, we can disable YAML compatibility if strict mode has been enabled. + if (!$config->get('system.strict_mode.yaml_compat', true)) { + YamlFile::globalSettings(['compat' => false, 'native' => true]); + } + + return $config; }; $container['languages'] = function ($c) { @@ -65,6 +73,10 @@ public static function blueprints(Container $container) return $blueprints->name("master-{$setup->environment}")->load(); } + /** + * @param Container $container + * @return Config + */ public static function load(Container $container) { /** Setup $setup */ diff --git a/system/src/Grav/Common/Service/PageServiceProvider.php b/system/src/Grav/Common/Service/PageServiceProvider.php index bf55f791b4..ccfa58089a 100644 --- a/system/src/Grav/Common/Service/PageServiceProvider.php +++ b/system/src/Grav/Common/Service/PageServiceProvider.php @@ -8,6 +8,7 @@ namespace Grav\Common\Service; +use Grav\Common\Config\Config; use Grav\Common\Grav; use Grav\Common\Language\Language; use Grav\Common\Page\Page; @@ -26,35 +27,33 @@ public function register(Container $container) /** @var Pages $pages */ $pages = $c['pages']; + /** @var Config $config */ + $config = $c['config']; + /** @var Uri $uri */ $uri = $c['uri']; - $path = $uri->path(); // Don't trim to support trailing slash default routes - $path = $path ?: '/'; - + $path = $uri->path() ?: '/'; // Don't trim to support trailing slash default routes $page = $pages->dispatch($path); // Redirection tests if ($page) { - /** @var Language $language */ - $language = $c['language']; - // some debugger override logic if ($page->debugger() === false) { $c['debugger']->enabled(false); } - if ($c['config']->get('system.force_ssl')) { - if (!isset($_SERVER['HTTPS']) || $_SERVER["HTTPS"] != "on") { - $url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]; + if ($config->get('system.force_ssl')) { + if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { + $url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; $c->redirect($url); } } - $url = $page->route(); + $url = $pages->route($page->route()); if ($uri->params()) { - if ($url == '/') { //Avoid double slash + if ($url === '/') { //Avoid double slash $url = $uri->params(); } else { $url .= $uri->params(); @@ -67,18 +66,16 @@ public function register(Container $container) $url .= '#' . $uri->fragment(); } + /** @var Language $language */ + $language = $c['language']; + // Language-specific redirection scenarios - if ($language->enabled()) { - if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) { - $c->redirect($url); - } - if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) { - $c->redirectLangSafe($url); - } + if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) { + $c->redirect($url); } // Default route test and redirect - if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) { - $c->redirectLangSafe($url); + if ($config->get('system.pages.redirect_default_route') && $page->route() !== $path) { + $c->redirect($url); } } diff --git a/system/src/Grav/Common/Service/SessionServiceProvider.php b/system/src/Grav/Common/Service/SessionServiceProvider.php index 52e107b7aa..9262efa829 100644 --- a/system/src/Grav/Common/Service/SessionServiceProvider.php +++ b/system/src/Grav/Common/Service/SessionServiceProvider.php @@ -29,21 +29,22 @@ public function register(Container $container) /** @var Uri $uri */ $uri = $c['uri']; - // Get session parameters. - $session_timeout = (int)$config->get('system.session.timeout', 1800); - $session_path = $config->get('system.session.path'); - if (null === $session_path) { - $session_path = '/' . ltrim(Uri::filterPath($uri->rootUrl(false)), '/'); - } - $domain = $uri->host(); - if ($domain === 'localhost') { - $domain = ''; - } - // Get session options. - $secure = (bool)$config->get('system.session.secure', false); - $httponly = (bool)$config->get('system.session.httponly', true); $enabled = (bool)$config->get('system.session.enabled', false); + $cookie_secure = (bool)$config->get('system.session.secure', false); + $cookie_httponly = (bool)$config->get('system.session.httponly', true); + $cookie_lifetime = (int)$config->get('system.session.timeout', 1800); + $cookie_path = $config->get('system.session.path'); + if (null === $cookie_path) { + $cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/'); + } + // Session cookie path requires trailing slash. + $cookie_path = rtrim($cookie_path, '/') . '/'; + + $cookie_domain = $uri->host(); + if ($cookie_domain === 'localhost') { + $cookie_domain = ''; + } // Activate admin if we're inside the admin path. $is_admin = false; @@ -56,14 +57,14 @@ public function register(Container $container) // Check no language, simple language prefix (en) and region specific language prefix (en-US). $pos = strpos($current_route, $base); if ($pos === 0 || $pos === 3 || $pos === 6) { - $session_timeout = $config->get('plugins.admin.session.timeout', 1800); + $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); $enabled = $is_admin = true; } } // Fix for HUGE session timeouts. - if ($session_timeout > 99999999999) { - $session_timeout = 9999999999; + if ($cookie_lifetime > 99999999999) { + $cookie_lifetime = 9999999999; } $inflector = new Inflector(); @@ -73,10 +74,16 @@ public function register(Container $container) } // Define session service. - $session = new Session($session_timeout, $session_path, $domain); - $session->setName($session_name); - $session->setSecure($secure); - $session->setHttpOnly($httponly); + $options = [ + 'name' => $session_name, + 'cookie_lifetime' => $cookie_lifetime, + 'cookie_path' => $cookie_path, + 'cookie_domain' => $cookie_domain, + 'cookie_secure' => $cookie_secure, + 'cookie_httponly' => $cookie_httponly + ] + (array) $config->get('system.session.options'); + + $session = new Session($options); $session->setAutoStart($enabled); return $session; diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index 7573f08660..f7221194fd 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -8,34 +8,18 @@ namespace Grav\Common; -use RocketTheme\Toolbox\Session\Session as BaseSession; - -class Session extends BaseSession +class Session extends \Grav\Framework\Session\Session { /** @var bool */ protected $autoStart = false; - protected $lifetime; - protected $path; - protected $domain; - protected $secure; - protected $httpOnly; - /** - * @param int $lifetime Defaults to 1800 seconds. - * @param string $path Cookie path. - * @param string $domain Optional, domain for the session - * @throws \RuntimeException + * @return \Grav\Framework\Session\Session + * @deprecated 1.5 */ - public function __construct($lifetime, $path, $domain = null) + public static function instance() { - $this->lifetime = $lifetime; - $this->path = $path; - $this->domain = $domain; - - if (php_sapi_name() !== 'cli') { - parent::__construct($lifetime, $path, $domain); - } + return static::getInstance(); } /** @@ -48,9 +32,6 @@ public function init() if ($this->autoStart) { $this->start(); - // TODO: This setcookie shouldn't be here, session should by itself be able to update its cookie. - setcookie(session_name(), session_id(), $this->lifetime ? time() + $this->lifetime : 0, $this->path, $this->domain, $this->secure, $this->httpOnly); - $this->autoStart = false; } } @@ -67,27 +48,25 @@ public function setAutoStart($auto) } /** - * @param bool $secure - * @return $this + * Returns attributes. + * + * @return array Attributes + * @deprecated 1.5 */ - public function setSecure($secure) + public function all() { - $this->secure = $secure; - ini_set('session.cookie_secure', (bool)$secure); - - return $this; + return $this->getAll(); } /** - * @param bool $httpOnly - * @return $this + * Checks if the session was started. + * + * @return Boolean + * @deprecated 1.5 */ - public function setHttpOnly($httpOnly) + public function started() { - $this->httpOnly = $httpOnly; - ini_set('session.cookie_httponly', (bool)$httpOnly); - - return $this; + return $this->isStarted(); } /** diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index 8f29037dd7..bf86d8dc66 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -98,7 +98,7 @@ public function all() continue; } - $theme = $directory->getBasename(); + $theme = $directory->getFilename(); $result = self::get($theme); if ($result) { diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php index 5943c952a7..0aa12eae54 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php @@ -12,7 +12,7 @@ class TwigNodeMarkdown extends \Twig_Node implements \Twig_NodeOutputInterface { public function __construct(\Twig_Node $body, $lineno, $tag = 'markdown') { - parent::__construct(array('body' => $body), array(), $lineno, $tag); + parent::__construct(['body' => $body], [], $lineno, $tag); } /** * Compiles the node to PHP. diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php index e0e8127006..1630e5eaef 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php @@ -24,20 +24,17 @@ public function compile(\Twig_Compiler $compiler) { $compiler ->addDebugInfo($this) - ->write("switch (") + ->write('switch (') ->subcompile($this->getNode('value')) ->raw(") {\n") ->indent(); - foreach ($this->getNode('cases') as $case) - { - if (!$case->hasNode('body')) - { + foreach ($this->getNode('cases') as $case) { + if (!$case->hasNode('body')) { continue; } - foreach ($case->getNode('values') as $value) - { + foreach ($case->getNode('values') as $value) { $compiler ->write('case ') ->subcompile($value) @@ -53,8 +50,7 @@ public function compile(\Twig_Compiler $compiler) ->write("}\n"); } - if ($this->hasNode('default') && $this->getNode('default') !== null) - { + if ($this->hasNode('default') && $this->getNode('default') !== null) { $compiler ->write("default:\n") ->write("{\n") diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php index 2da932de4c..0768b3071e 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php @@ -37,8 +37,7 @@ public function parse(\Twig_Token $token) $stream->expect(\Twig_Token::BLOCK_END_TYPE); // There can be some whitespace between the {% switch %} and first {% case %} tag. - while ($stream->getCurrent()->getType() == \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) == '') - { + while ($stream->getCurrent()->getType() === \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') { $stream->next(); } @@ -47,56 +46,45 @@ public function parse(\Twig_Token $token) $expressionParser = $this->parser->getExpressionParser(); $default = null; - $cases = array(); + $cases = []; $end = false; - while (!$end) - { + while (!$end) { $next = $stream->next(); - switch ($next->getValue()) - { + switch ($next->getValue()) { case 'case': - { - $values = array(); - - while (true) - { - $values[] = $expressionParser->parsePrimaryExpression(); - // Multiple allowed values? - if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or')) - { - $stream->next(); - } - else - { - break; - } + $values = []; + + while (true) { + $values[] = $expressionParser->parsePrimaryExpression(); + // Multiple allowed values? + if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or')) { + $stream->next(); + } else { + break; } - - $stream->expect(\Twig_Token::BLOCK_END_TYPE); - $body = $this->parser->subparse(array($this, 'decideIfFork')); - $cases[] = new \Twig_Node(array( - 'values' => new \Twig_Node($values), - 'body' => $body - )); - break; } + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideIfFork')); + $cases[] = new \Twig_Node([ + 'values' => new \Twig_Node($values), + 'body' => $body + ]); + break; + case 'default': - { - $stream->expect(\Twig_Token::BLOCK_END_TYPE); - $default = $this->parser->subparse(array($this, 'decideIfEnd')); - break; - } + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $default = $this->parser->subparse(array($this, 'decideIfEnd')); + break; + case 'endswitch': - { - $end = true; - break; - } + $end = true; + break; + default: - { - throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1); - } + throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1); } } @@ -127,7 +115,6 @@ public function decideIfEnd(\Twig_Token $token) return $token->test(array('endswitch')); } - /** * {@inheritdoc} */ diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index a47a2b7aa8..a69c1623db 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -113,7 +113,10 @@ public function init() $params['cache'] = new \Twig_Cache_Filesystem($cachePath, \Twig_Cache_Filesystem::FORCE_BYTECODE_INVALIDATION); } - if (!empty($this->autoescape)) { + if (!$config->get('system.strict_mode.twig_compat', true)) { + // Force autoescape on for all files if in strict mode. + $params['autoescape'] = true; + } elseif (!empty($this->autoescape)) { $params['autoescape'] = $this->autoescape; } @@ -238,7 +241,7 @@ public function processPage(Page $item, $content = null) // Process Modular Twig if ($item->modularTwig()) { $twig_vars['content'] = $content; - $extension = $this->grav['uri']->extension(); + $extension = $item->templateFormat(); $extension = $extension ? ".{$extension}.twig" : TEMPLATE_EXT; $template = $item->template() . $extension; $output = $content = $local_twig->render($template, $twig_vars); diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index fced9158b8..3152656dc5 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -72,7 +72,7 @@ public function getFilters() new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']), new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']), new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']), - new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction']), + new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]), new \Twig_SimpleFilter('md5', [$this, 'md5Filter']), new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']), new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']), @@ -88,9 +88,6 @@ public function getFilters() new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']), new \Twig_SimpleFilter('sort_by_key', [$this, 'sortByKeyFilter']), new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']), - new \Twig_SimpleFilter('t', [$this, 'translate']), - new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']), - new \Twig_SimpleFilter('ta', [$this, 'translateArray']), new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']), new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']), new \Twig_SimpleFilter('json_decode', [$this, 'jsonDecodeFilter']), @@ -100,6 +97,18 @@ public function getFilters() new \Twig_SimpleFilter('print_r', 'print_r'), new \Twig_SimpleFilter('yaml_encode', [$this, 'yamlEncodeFilter']), new \Twig_SimpleFilter('yaml_decode', [$this, 'yamlDecodeFilter']), + + // Translations + new \Twig_SimpleFilter('t', [$this, 'translate']), + new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']), + new \Twig_SimpleFilter('ta', [$this, 'translateArray']), + + // Casting values + new \Twig_SimpleFilter('string', [$this, 'stringFilter']), + new \Twig_SimpleFilter('int', [$this, 'intFilter'], ['is_safe' => true]), + new \Twig_SimpleFilter('bool', [$this, 'boolFilter']), + new \Twig_SimpleFilter('float', [$this, 'floatFilter'], ['is_safe' => true]), + new \Twig_SimpleFilter('array', [$this, 'arrayFilter']), ]; } @@ -111,7 +120,7 @@ public function getFilters() public function getFunctions() { return [ - new \Twig_SimpleFunction('array', [$this, 'arrayFunc']), + new \Twig_SimpleFunction('array', [$this, 'arrayFilter']), new \Twig_SimpleFunction('array_key_value', [$this, 'arrayKeyValueFunc']), new \Twig_SimpleFunction('array_key_exists', 'array_key_exists'), new \Twig_SimpleFunction('array_unique', 'array_unique'), @@ -132,9 +141,6 @@ public function getFunctions() new \Twig_SimpleFunction('regex_replace', [$this, 'regexReplace']), new \Twig_SimpleFunction('regex_filter', [$this, 'regexFilter']), new \Twig_SimpleFunction('string', [$this, 'stringFunc']), - new \Twig_simpleFunction('t', [$this, 'translate']), - new \Twig_simpleFunction('tl', [$this, 'translateLanguage']), - new \Twig_simpleFunction('ta', [$this, 'translateArray']), new \Twig_SimpleFunction('url', [$this, 'urlFunc']), new \Twig_SimpleFunction('json_decode', [$this, 'jsonDecodeFilter']), new \Twig_SimpleFunction('get_cookie', [$this, 'getCookie']), @@ -151,6 +157,10 @@ public function getFunctions() new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']), new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFilter']), + // Translations + new \Twig_simpleFunction('t', [$this, 'translate']), + new \Twig_simpleFunction('tl', [$this, 'translateLanguage']), + new \Twig_simpleFunction('ta', [$this, 'translateArray']), ]; } @@ -617,6 +627,62 @@ public function ltrimFilter($value, $chars = null) return ltrim($value, $chars); } + /** + * Casts input to string. + * + * @param mixed $input + * @return string + */ + public function stringFilter($input) + { + return (string) $input; + } + + + /** + * Casts input to int. + * + * @param mixed $input + * @return int + */ + public function intFilter($input) + { + return (int) $input; + } + + /** + * Casts input to bool. + * + * @param mixed $input + * @return bool + */ + public function boolFilter($input) + { + return (bool) $input; + } + + /** + * Casts input to float. + * + * @param mixed $input + * @return float + */ + public function floatFilter($input) + { + return (float) $input; + } + + /** + * Casts input to array. + * + * @param mixed $input + * @return array + */ + public function arrayFilter($input) + { + return (array) $input; + } + /** * @return mixed */ @@ -693,7 +759,6 @@ public function evaluateTwigFunc($context, $twig ) { $template = $env->createTemplate($twig); return $template->render($context); -; } /** @@ -748,7 +813,7 @@ public function dump(\Twig_Environment $env, $context) * Output a Gist * * @param string $id - * @param string $file + * @param string|bool $file * * @return string */ @@ -788,19 +853,6 @@ public static function padFilter($input, $pad_length, $pad_string = " ", $pad_ty return str_pad($input, (int)$pad_length, $pad_string, $pad_type); } - - /** - * Cast a value to array - * - * @param $value - * - * @return array - */ - public function arrayFunc($value) - { - return (array)$value; - } - /** * Workaround for twig associative array initialization * Returns a key => val array @@ -976,7 +1028,7 @@ public function regexFilter($array, $regex, $flags = 0) { public function redirectFunc($url, $statusCode = 303) { header('Location: ' . $url, true, $statusCode); - die(); + exit(); } /** @@ -1060,7 +1112,7 @@ public function readFileFunc($filepath) if (file_exists($filepath)) { return file_get_contents($filepath); - } + } return false; } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 8017ecb884..4bb6bc7b1d 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -11,6 +11,7 @@ use Grav\Common\Config\Config; use Grav\Common\Language\Language; use Grav\Common\Page\Page; +use Grav\Common\Page\Pages; use Grav\Framework\Route\RouteFactory; use Grav\Framework\Uri\UriFactory; use Grav\Framework\Uri\UriPartsFilter; @@ -156,12 +157,6 @@ public function init() $uri = preg_replace('|^' . preg_quote($setup_base, '|') . '|', '', $uri); } - // If configured to, redirect trailing slash URI's with a 302 redirect - $redirect = str_replace($this->root, '', rtrim($uri, '/')); - if ($redirect && $uri !== '/' && $redirect !== $this->base() && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($uri, '/')) { - $grav->redirect($redirect, 302); - } - // process params $uri = $this->processParams($uri, $config->get('system.param_sep')); @@ -206,9 +201,9 @@ public function init() } // Set some Grav stuff - $grav['base_url_absolute'] = $grav['config']->get('system.custom_base_url') ?: $this->rootUrl(true); + $grav['base_url_absolute'] = $config->get('system.custom_base_url') ?: $this->rootUrl(true); $grav['base_url_relative'] = $this->rootUrl(false); - $grav['base_url'] = $grav['config']->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative']; + $grav['base_url'] = $config->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative']; RouteFactory::setRoot($this->root_path); RouteFactory::setLanguage($language->getLanguageURLPrefix()); @@ -376,6 +371,17 @@ public function extension($default = null) return $this->extension; } + public function method() + { + $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + + if ($method === 'POST' && isset($_SERVER['X-HTTP-METHOD-OVERRIDE'])) { + $method = strtoupper($_SERVER['X-HTTP-METHOD-OVERRIDE']); + } + + return $method; + } + /** * Return the scheme of the URI * @@ -481,11 +487,9 @@ public function uri($include_root = true) { if ($include_root) { return $this->uri; - } else { - $uri = str_replace($this->root_path, '', $this->uri); - return $uri; } + return str_replace($this->root_path, '', $this->uri); } /** @@ -508,16 +512,10 @@ public function baseIncludingLanguage() { $grav = Grav::instance(); - // Link processing should prepend language - $language = $grav['language']; - $language_append = ''; - if ($language->enabled()) { - $language_append = $language->getLanguageURLPrefix(); - } + /** @var Pages $pages */ + $pages = $grav['pages']; - $base = $grav['base_url_relative']; - - return rtrim($base . $grav['pages']->base(), '/') . $language_append; + return $pages->baseUrl(null, false); } /** @@ -633,10 +631,9 @@ public static function ip() } return $ip; - } - /** + /** * Returns current Uri. * * @return \Grav\Framework\Uri\Uri @@ -883,7 +880,26 @@ public static function convertUrl(Page $page, $url, $type = 'link', $absolute = public static function parseUrl($url) { $grav = Grav::instance(); - $parts = parse_url($url); + + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + function ($matches) { return rawurlencode($matches[0]); }, + $url + ); + + $parts = parse_url($encodedUrl); + + if (false === $parts) { + return false; + } + + foreach($parts as $name => $value) { + $parts[$name] = rawurldecode($value); + } + + if (!isset($parts['path'])) { + $parts['path'] = ''; + } list($stripped_path, $params) = static::extractParams($parts['path'], $grav['config']->get('system.param_sep')); @@ -1262,7 +1278,7 @@ public function post($element = null, $filter_type = null) { if (!$this->post) { $content_type = $this->getContentType(); - if ($content_type == 'application/json') { + if ($content_type === 'application/json') { $json = file_get_contents('php://input'); $this->post = json_decode($json, true); } elseif (!empty($_POST)) { @@ -1270,7 +1286,7 @@ public function post($element = null, $filter_type = null) } } - if ($this->post && !is_null($element)) { + if ($this->post && null !== $element) { $item = Utils::getDotNotation($this->post, $element); if ($filter_type) { $item = filter_var($item, $filter_type); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 02d5e158a3..0f27277b3c 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -45,8 +45,20 @@ public static function url($input, $domain = false) /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; - // Get relative path to the resource (or false if not found). - $resource = $locator->findResource($input, false); + $parts = Uri::parseUrl($input); + + if ($parts) { + $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); + + if (isset($parts['query'])) { + $resource = $resource . '?' . $parts['query']; + } + } else { + // Not a valid URL (can still be a stream). + $resource = $locator->findResource($input, false); + } + + } else { $resource = $input; } @@ -262,7 +274,7 @@ public static function truncate($string, $limit = 150, $up_to_break = false, $br // is $break present between $limit and the end of the string? if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) { if ($breakpoint < mb_strlen($string) - 1) { - $string = mb_substr($string, 0, $breakpoint) . $break; + $string = mb_substr($string, 0, $breakpoint) . $pad; } } else { $string = mb_substr($string, 0, $limit) . $pad; @@ -705,11 +717,11 @@ public static function isPositive($value) * with reverse proxy setups. * * @param string $action - * @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * * @return string the nonce string */ - private static function generateNonceString($action, $plusOneTick = false) + private static function generateNonceString($action, $previousTick = false) { $username = ''; if (isset(Grav::instance()['user'])) { @@ -720,29 +732,8 @@ private static function generateNonceString($action, $plusOneTick = false) $token = session_id(); $i = self::nonceTick(); - if ($plusOneTick) { - $i++; - } - - return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt')); - } - - //Added in version 1.0.8 to ensure that existing nonces are not broken. - private static function generateNonceStringOldStyle($action, $plusOneTick = false) - { - if (isset(Grav::instance()['user'])) { - $user = Grav::instance()['user']; - $username = $user->username; - if (isset($_SERVER['REMOTE_ADDR'])) { - $username .= $_SERVER['REMOTE_ADDR']; - } - } else { - $username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; - } - $token = session_id(); - $i = self::nonceTick(); - if ($plusOneTick) { - $i++; + if ($previousTick) { + $i--; } return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt')); @@ -768,33 +759,20 @@ private static function nonceTick() * action is the same for 12 hours. * * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) - * @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * * @return string the nonce */ - public static function getNonce($action, $plusOneTick = false) - { - // Don't regenerate this again if not needed - if (isset(static::$nonces[$action])) { - return static::$nonces[$action]; - } - $nonce = md5(self::generateNonceString($action, $plusOneTick)); - static::$nonces[$action] = $nonce; - - return static::$nonces[$action]; - } - - //Added in version 1.0.8 to ensure that existing nonces are not broken. - public static function getNonceOldStyle($action, $plusOneTick = false) + public static function getNonce($action, $previousTick = false) { // Don't regenerate this again if not needed - if (isset(static::$nonces[$action])) { - return static::$nonces[$action]; + if (isset(static::$nonces[$action][$previousTick])) { + return static::$nonces[$action][$previousTick]; } - $nonce = md5(self::generateNonceStringOldStyle($action, $plusOneTick)); - static::$nonces[$action] = $nonce; + $nonce = md5(self::generateNonceString($action, $previousTick)); + static::$nonces[$action][$previousTick] = $nonce; - return static::$nonces[$action]; + return static::$nonces[$action][$previousTick]; } /** @@ -818,20 +796,8 @@ public static function verifyNonce($nonce, $action) } //Nonce generated 12-24 hours ago - $plusOneTick = true; - if ($nonce === self::getNonce($action, $plusOneTick)) { - return true; - } - - //Added in version 1.0.8 to ensure that existing nonces are not broken. - //Nonce generated 0-12 hours ago - if ($nonce === self::getNonceOldStyle($action)) { - return true; - } - - //Nonce generated 12-24 hours ago - $plusOneTick = true; - if ($nonce === self::getNonceOldStyle($action, $plusOneTick)) { + $previousTick = true; + if ($nonce === self::getNonce($action, $previousTick)) { return true; } diff --git a/system/src/Grav/Console/Cli/InstallCommand.php b/system/src/Grav/Console/Cli/InstallCommand.php index ac00256dd5..afcd80c02e 100644 --- a/system/src/Grav/Console/Cli/InstallCommand.php +++ b/system/src/Grav/Console/Cli/InstallCommand.php @@ -9,9 +9,9 @@ namespace Grav\Console\Cli; use Grav\Console\ConsoleCommand; +use RocketTheme\Toolbox\File\YamlFile; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Yaml\Yaml; class InstallCommand extends ConsoleCommand { @@ -71,9 +71,9 @@ protected function serve() // Look for dependencies file in ROOT and USER dir if (file_exists($this->user_path . $dependencies_file)) { - $this->config = Yaml::parse(file_get_contents($this->user_path . $dependencies_file)); + $file = YamlFile::instance($this->user_path . $dependencies_file); } elseif (file_exists($this->destination . $dependencies_file)) { - $this->config = Yaml::parse(file_get_contents($this->destination . $dependencies_file)); + $file = YamlFile::instance($this->destination . $dependencies_file); } else { $this->output->writeln('ERROR Missing .dependencies file in user/ folder'); if ($this->input->getArgument('destination')) { @@ -85,6 +85,9 @@ protected function serve() return; } + $this->config = $file->content(); + $file->free(); + // If yaml config, process if ($this->config) { if (!$this->input->getOption('symlink')) { @@ -153,23 +156,32 @@ private function symlink() exec('cd ' . $this->destination); foreach ($this->config['links'] as $repo => $data) { - $from = $this->local_config[$data['scm'] . '_repos'] . $data['src']; + $repos = (array) $this->local_config[$data['scm'] . '_repos']; + $from = false; $to = $this->destination . $data['path']; - if (file_exists($from)) { - if (!file_exists($to)) { - symlink($from, $to); - $this->output->writeln('SUCCESS symlinked ' . $data['src'] . ' -> ' . $data['path'] . ''); - $this->output->writeln(''); - } else { - $this->output->writeln('destination: ' . $to . ' already exists, skipping...'); - $this->output->writeln(''); + foreach ($repos as $repo) { + $path = $repo . $data['src']; + if (file_exists($path)) { + $from = $path; + continue; } - } else { + } + + if (!$from) { $this->output->writeln('source: ' . $from . ' does not exists, skipping...'); $this->output->writeln(''); } + if (!file_exists($to)) { + symlink($from, $to); + $this->output->writeln('SUCCESS symlinked ' . $data['src'] . ' -> ' . $data['path'] . ''); + $this->output->writeln(''); + } else { + $this->output->writeln('destination: ' . $to . ' already exists, skipping...'); + $this->output->writeln(''); + } + } } } diff --git a/system/src/Grav/Console/ConsoleTrait.php b/system/src/Grav/Console/ConsoleTrait.php index b6ab3bfe0d..4498172450 100644 --- a/system/src/Grav/Console/ConsoleTrait.php +++ b/system/src/Grav/Console/ConsoleTrait.php @@ -12,11 +12,11 @@ use Grav\Common\Composer; use Grav\Common\GravTrait; use Grav\Console\Cli\ClearCacheCommand; +use RocketTheme\Toolbox\File\YamlFile; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Yaml\Yaml; trait ConsoleTrait { @@ -123,7 +123,9 @@ public function loadLocalConfig() $local_config_file = $home_folder . '/.grav/config'; if (file_exists($local_config_file)) { - $this->local_config = Yaml::parse(file_get_contents($local_config_file)); + $file = YamlFile::instance($local_config_file); + $this->local_config = $file->content(); + $file->free(); return $local_config_file; } diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php index d7e932e4ec..0311428254 100644 --- a/system/src/Grav/Console/Gpm/InstallCommand.php +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -444,18 +444,21 @@ private function getSymlinkSource($package) { $matches = $this->getGitRegexMatches($package); - foreach ($this->local_config as $path) { + foreach ($this->local_config as $paths) { if (Utils::endsWith($matches[2], '.git')) { $repo_dir = preg_replace('/\.git$/', '', $matches[2]); } else { $repo_dir = $matches[2]; } - - $from = rtrim($path, '/') . '/' . $repo_dir; - - if (file_exists($from)) { - return $from; + + $paths = (array) $paths; + foreach ($paths as $repo) { + $path = rtrim($repo, '/') . '/' . $repo_dir; + if (file_exists($path)) { + return $path; + } } + } return false; diff --git a/system/src/Grav/Console/Gpm/VersionCommand.php b/system/src/Grav/Console/Gpm/VersionCommand.php index c828106c48..eb7872ead8 100644 --- a/system/src/Grav/Console/Gpm/VersionCommand.php +++ b/system/src/Grav/Console/Gpm/VersionCommand.php @@ -11,9 +11,9 @@ use Grav\Common\GPM\GPM; use Grav\Common\GPM\Upgrader; use Grav\Console\ConsoleCommand; +use RocketTheme\Toolbox\File\YamlFile; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Yaml\Yaml; class VersionCommand extends ConsoleCommand { @@ -84,7 +84,10 @@ protected function serve() } } - $package_yaml = Yaml::parse(file_get_contents($blueprints_path)); + $file = YamlFile::instance($blueprints_path); + $package_yaml = $file->content(); + $file->free(); + $version = $package_yaml['version']; if (!$version) { diff --git a/system/src/Grav/Framework/Cache/CacheTrait.php b/system/src/Grav/Framework/Cache/CacheTrait.php index 65f4d8d40b..2f2b178baa 100644 --- a/system/src/Grav/Framework/Cache/CacheTrait.php +++ b/system/src/Grav/Framework/Cache/CacheTrait.php @@ -16,21 +16,18 @@ */ trait CacheTrait { - /** - * @var string - */ + /** @var string */ private $namespace = ''; - /** - * @var int|null - */ + /** @var int|null */ private $defaultLifetime = null; - /** - * @var \stdClass - */ + /** @var \stdClass */ private $miss; + /** @var bool */ + private $validation = true; + /** * Always call from constructor. * @@ -45,6 +42,14 @@ protected function init($namespace = '', $defaultLifetime = null) $this->miss = new \stdClass; } + /** + * @param $validation + */ + public function setValidation($validation) + { + $this->validation = (bool) $validation; + } + /** * @return string */ @@ -307,6 +312,10 @@ protected function validateKey($key) */ protected function validateKeys($keys) { + if (!$this->validation) { + return; + } + foreach ($keys as $key) { $this->validateKey($key); } diff --git a/system/src/Grav/Framework/Collection/ArrayCollection.php b/system/src/Grav/Framework/Collection/ArrayCollection.php index bf6dda056d..4c922f4f05 100644 --- a/system/src/Grav/Framework/Collection/ArrayCollection.php +++ b/system/src/Grav/Framework/Collection/ArrayCollection.php @@ -24,11 +24,6 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface */ public function reverse() { - // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4). - if (!method_exists($this, 'createFrom')) { - return new static(array_reverse($this->toArray())); - } - return $this->createFrom(array_reverse($this->toArray())); } @@ -42,11 +37,6 @@ public function shuffle() $keys = $this->getKeys(); shuffle($keys); - // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4). - if (!method_exists($this, 'createFrom')) { - return new static(array_replace(array_flip($keys), $this->toArray())); - } - return $this->createFrom(array_replace(array_flip($keys), $this->toArray())); } diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlock.php b/system/src/Grav/Framework/ContentBlock/ContentBlock.php index 6229b81518..672dba4cc6 100644 --- a/system/src/Grav/Framework/ContentBlock/ContentBlock.php +++ b/system/src/Grav/Framework/ContentBlock/ContentBlock.php @@ -27,6 +27,7 @@ class ContentBlock implements ContentBlockInterface protected $tokenTemplate = '@@BLOCK-%s@@'; protected $content = ''; protected $blocks = []; + protected $checksum; /** * @param string $id @@ -40,6 +41,7 @@ public static function create($id = null) /** * @param array $serialized * @return ContentBlockInterface + * @throws \InvalidArgumentException */ public static function fromArray(array $serialized) { @@ -48,14 +50,14 @@ public static function fromArray(array $serialized) $id = isset($serialized['id']) ? $serialized['id'] : null; if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) { - throw new \RuntimeException('Bad data'); + throw new \InvalidArgumentException('Bad data'); } /** @var ContentBlockInterface $instance */ $instance = new $type($id); $instance->build($serialized); } catch (\Exception $e) { - throw new \RuntimeException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e); + throw new \InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e); } return $instance; @@ -104,9 +106,13 @@ public function toArray() $array = [ '_type' => get_class($this), '_version' => $this->version, - 'id' => $this->id, + 'id' => $this->id ]; + if ($this->checksum) { + $array['checksum'] = $this->checksum; + } + if ($this->content) { $array['content'] = $this->content; } @@ -158,6 +164,7 @@ public function build(array $serialized) $this->checkVersion($serialized); $this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId(); + $this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null; if (isset($serialized['content'])) { $this->setContent($serialized['content']); @@ -169,6 +176,25 @@ public function build(array $serialized) } } + /** + * @param string $checksum + * @return $this + */ + public function setChecksum($checksum) + { + $this->checksum = $checksum; + + return $this; + } + + /** + * @return string + */ + public function getChecksum() + { + return $this->checksum; + } + /** * @param string $content * @return $this @@ -222,7 +248,7 @@ protected function generateId() */ protected function checkVersion(array $serialized) { - $version = isset($serialized['_version']) ? (string) $serialized['_version'] : '1'; + $version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1; if ($version !== $this->version) { throw new \RuntimeException(sprintf('Unsupported version %s', $version)); } diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php index 4f2434c9d2..fff8f2e73f 100644 --- a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php +++ b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php @@ -61,6 +61,17 @@ public function __toString(); */ public function build(array $serialized); + /** + * @param string $checksum + * @return $this + */ + public function setChecksum($checksum); + + /** + * @return string + */ + public function getChecksum(); + /** * @param string $content * @return $this diff --git a/system/src/Grav/Framework/ContentBlock/HtmlBlock.php b/system/src/Grav/Framework/ContentBlock/HtmlBlock.php index 976018a0e0..3fd345b8c7 100644 --- a/system/src/Grav/Framework/ContentBlock/HtmlBlock.php +++ b/system/src/Grav/Framework/ContentBlock/HtmlBlock.php @@ -15,6 +15,7 @@ */ class HtmlBlock extends ContentBlock implements HtmlBlockInterface { + protected $version = 1; protected $frameworks = []; protected $styles = []; protected $scripts = []; diff --git a/system/src/Grav/Framework/File/Formatter/FormatterInterface.php b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php new file mode 100644 index 0000000000..273bf7873a --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php @@ -0,0 +1,44 @@ +config = $config + [ + 'file_extension' => '.ini' + ]; + } + + /** + * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + */ + public function getFileExtension() + { + return $this->getDefaultFileExtension(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFileExtension() + { + $extensions = $this->getSupportedFileExtensions(); + + return (string) reset($extensions); + } + + /** + * {@inheritdoc} + */ + public function getSupportedFileExtensions() + { + return (array) $this->config['file_extension']; + } + + /** + * {@inheritdoc} + */ + public function encode($data) + { + $string = ''; + foreach ($data as $key => $value) { + $string .= $key . '="' . preg_replace( + ['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"], + ['\"', '\\\\', '\t', '\n', '\r'], + $value + ) . "\"\n"; + } + + return $string; + } + + /** + * {@inheritdoc} + */ + public function decode($data) + { + $decoded = @parse_ini_string($data); + + if ($decoded === false) { + throw new \RuntimeException('Decoding INI failed'); + } + + return $decoded; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/JsonFormatter.php b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php new file mode 100644 index 0000000000..f5705e3814 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php @@ -0,0 +1,78 @@ +config = $config + [ + 'file_extension' => '.json', + 'encode_options' => 0, + 'decode_assoc' => true + ]; + } + + /** + * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + */ + public function getFileExtension() + { + return $this->getDefaultFileExtension(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFileExtension() + { + $extensions = $this->getSupportedFileExtensions(); + + return (string) reset($extensions); + } + + /** + * {@inheritdoc} + */ + public function getSupportedFileExtensions() + { + return (array) $this->config['file_extension']; + } + + /** + * {@inheritdoc} + */ + public function encode($data) + { + $encoded = @json_encode($data, $this->config['encode_options']); + + if ($encoded === false) { + throw new \RuntimeException('Encoding JSON failed'); + } + + return $encoded; + } + + /** + * {@inheritdoc} + */ + public function decode($data) + { + $decoded = @json_decode($data, $this->config['decode_assoc']); + + if ($decoded === false) { + throw new \RuntimeException('Decoding JSON failed'); + } + + return $decoded; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php new file mode 100644 index 0000000000..9cccba9903 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php @@ -0,0 +1,116 @@ +config = $config + [ + 'file_extension' => '.md', + 'header' => 'header', + 'body' => 'markdown', + 'raw' => 'frontmatter', + 'yaml' => ['inline' => 20] + ]; + + $this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']); + } + + /** + * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + */ + public function getFileExtension() + { + return $this->getDefaultFileExtension(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFileExtension() + { + $extensions = $this->getSupportedFileExtensions(); + + return (string) reset($extensions); + } + + /** + * {@inheritdoc} + */ + public function getSupportedFileExtensions() + { + return (array) $this->config['file_extension']; + } + + /** + * {@inheritdoc} + */ + public function encode($data) + { + $headerVar = $this->config['header']; + $bodyVar = $this->config['body']; + + $header = isset($data[$headerVar]) ? (array) $data[$headerVar] : []; + $body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : ''; + + // Create Markdown file with YAML header. + $encoded = ''; + if ($header) { + $encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n"; + } + $encoded .= $body; + + // Normalize line endings to Unix style. + $encoded = preg_replace("/(\r\n|\r)/", "\n", $encoded); + + return $encoded; + } + + /** + * {@inheritdoc} + */ + public function decode($data) + { + $headerVar = $this->config['header']; + $bodyVar = $this->config['body']; + $rawVar = $this->config['raw']; + + $content = [ + $headerVar => [], + $bodyVar => '' + ]; + + $headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis"; + + // Normalize line endings to Unix style. + $data = preg_replace("/(\r\n|\r)/", "\n", $data); + + // Parse header. + preg_match($headerRegex, ltrim($data), $matches); + if(empty($matches)) { + $content[$bodyVar] = $data; + } else { + // Normalize frontmatter. + $frontmatter = preg_replace("/\n\t/", "\n ", $matches[1]); + if ($rawVar) { + $content[$rawVar] = $frontmatter; + } + $content[$headerVar] = $this->headerFormatter->decode($frontmatter); + $content[$bodyVar] = $matches[2]; + } + + return $content; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php new file mode 100644 index 0000000000..5fd20eb895 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php @@ -0,0 +1,96 @@ +config = $config + [ + 'file_extension' => '.ser' + ]; + } + + /** + * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + */ + public function getFileExtension() + { + return $this->getDefaultFileExtension(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFileExtension() + { + $extensions = $this->getSupportedFileExtensions(); + + return (string) reset($extensions); + } + + /** + * {@inheritdoc} + */ + public function getSupportedFileExtensions() + { + return (array) $this->config['file_extension']; + } + + /** + * {@inheritdoc} + */ + public function encode($data) + { + return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r'])); + } + + /** + * {@inheritdoc} + */ + public function decode($data) + { + $decoded = @unserialize($data); + + if ($decoded === false) { + throw new \RuntimeException('Decoding serialized data failed'); + } + + return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]); + } + + /** + * Preserve new lines, recursive function. + * + * @param mixed $data + * @param array $search + * @param array $replace + * @return mixed + */ + protected function preserveLines($data, $search, $replace) + { + if (is_string($data)) { + $data = str_replace($search, $replace, $data); + } elseif (is_array($data)) { + foreach ($data as &$value) { + $value = $this->preserveLines($value, $search, $replace); + } + unset($value); + } + + return $data; + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/File/Formatter/YamlFormatter.php b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php new file mode 100644 index 0000000000..858fbea5f5 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php @@ -0,0 +1,103 @@ +config = $config + [ + 'file_extension' => '.yaml', + 'inline' => 5, + 'indent' => 2, + 'native' => true, + 'compat' => true + ]; + } + + /** + * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + */ + public function getFileExtension() + { + return $this->getDefaultFileExtension(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFileExtension() + { + $extensions = $this->getSupportedFileExtensions(); + + return (string) reset($extensions); + } + + /** + * {@inheritdoc} + */ + public function getSupportedFileExtensions() + { + return (array) $this->config['file_extension']; + } + + /** + * {@inheritdoc} + */ + public function encode($data) + { + try { + return (string) YamlParser::dump( + $data, + $this->config['inline'], + $this->config['indent'], + YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE + ); + } catch (DumpException $e) { + throw new \RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e); + } + } + + /** + * {@inheritdoc} + */ + public function decode($data) + { + // Try native PECL YAML PHP extension first if available. + if ($this->config['native'] && function_exists('yaml_parse')) { + // Safely decode YAML. + $saved = @ini_get('yaml.decode_php'); + @ini_set('yaml.decode_php', 0); + $decoded = @yaml_parse($data); + @ini_set('yaml.decode_php', $saved); + + if ($decoded !== false) { + return (array) $decoded; + } + } + + try { + return (array) YamlParser::parse($data); + } catch (ParseException $e) { + if ($this->config['compat']) { + return (array) FallbackYamlParser::parse($data); + } + + throw new \RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php b/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php index f99a5ff200..6334397f00 100644 --- a/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php +++ b/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php @@ -32,11 +32,6 @@ public function copy() $list[$key] = is_object($value) ? clone $value : $value; } - // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4). - if (!method_exists($this, 'createFrom')) { - return new static($list); - } - return $this->createFrom($list); } @@ -170,12 +165,7 @@ public function collectionGroup($property) { $collections = []; foreach ($this->group($property) as $id => $elements) { - // TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4). - if (!method_exists($this, 'createFrom')) { - $collection = new static($elements); - } else { - $collection = $this->createFrom($elements); - } + $collection = $this->createFrom($elements); $collections[$id] = $collection; } diff --git a/system/src/Grav/Framework/Object/Base/ObjectTrait.php b/system/src/Grav/Framework/Object/Base/ObjectTrait.php index 6522e8eb11..c7363810fb 100644 --- a/system/src/Grav/Framework/Object/Base/ObjectTrait.php +++ b/system/src/Grav/Framework/Object/Base/ObjectTrait.php @@ -15,7 +15,7 @@ */ trait ObjectTrait { - static protected $prefix; + /** @var string */ static protected $type; /** @@ -23,18 +23,28 @@ trait ObjectTrait */ private $_key; + /** + * @return string + */ + protected function getTypePrefix() + { + return ''; + } + /** * @param bool $prefix * @return string */ public function getType($prefix = true) { + $type = $prefix ? $this->getTypePrefix() : ''; + if (static::$type) { - return ($prefix ? static::$prefix : '') . static::$type; + return $type . static::$type; } $class = get_class($this); - return ($prefix ? static::$prefix : '') . strtolower(substr($class, strrpos($class, '\\') + 1)); + return $type . strtolower(substr($class, strrpos($class, '\\') + 1)); } /** @@ -108,7 +118,7 @@ public function defProperty($property, $default) */ public function serialize() { - return serialize($this->jsonSerialize()); + return serialize($this->doSerialize()); } /** @@ -124,6 +134,14 @@ public function unserialize($serialized) $this->doUnserialize($data); } + /** + * @return array + */ + protected function doSerialize() + { + return $this->jsonSerialize(); + } + /** * @param array $serialized */ @@ -159,10 +177,13 @@ public function __toString() /** * @param string $key + * @return $this */ protected function setKey($key) { $this->_key = (string) $key; + + return $this; } abstract protected function doHasProperty($property); diff --git a/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php new file mode 100644 index 0000000000..5ad3897d40 --- /dev/null +++ b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php @@ -0,0 +1,198 @@ +{$accessor}(); + break; + } + } + + if ($op) { + $function = 'filter' . ucfirst(strtolower($op)); + if (method_exists(static::class, $function)) { + $value = static::$function($value); + } + } + + return $value; + } + + public static function filterLower($str) + { + return mb_strtolower($str); + } + + public static function filterUpper($str) + { + return mb_strtoupper($str); + } + + public static function filterLength($str) + { + return mb_strlen($str); + } + + public static function filterLtrim($str) + { + return ltrim($str); + } + + public static function filterRtrim($str) + { + return rtrim($str); + } + + public static function filterTrim($str) + { + return trim($str); + } + + /** + * Helper for sorting arrays of objects based on multiple fields + orientations. + * + * @param string $name + * @param int $orientation + * @param \Closure $next + * + * @return \Closure + */ + public static function sortByField($name, $orientation = 1, \Closure $next = null) + { + if (!$next) { + $next = function() { + return 0; + }; + } + + return function ($a, $b) use ($name, $next, $orientation) { + $aValue = static::getObjectFieldValue($a, $name); + $bValue = static::getObjectFieldValue($b, $name); + + if ($aValue === $bValue) { + return $next($a, $b); + } + + return (($aValue > $bValue) ? 1 : -1) * $orientation; + }; + } + + /** + * {@inheritDoc} + */ + public function walkComparison(Comparison $comparison) + { + $field = $comparison->getField(); + $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + + switch ($comparison->getOperator()) { + case Comparison::EQ: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) === $value; + }; + + case Comparison::NEQ: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) !== $value; + }; + + case Comparison::LT: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) < $value; + }; + + case Comparison::LTE: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) <= $value; + }; + + case Comparison::GT: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) > $value; + }; + + case Comparison::GTE: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) >= $value; + }; + + case Comparison::IN: + return function ($object) use ($field, $value) { + return \in_array(static::getObjectFieldValue($object, $field), $value, true); + }; + + case Comparison::NIN: + return function ($object) use ($field, $value) { + return !\in_array(static::getObjectFieldValue($object, $field), $value, true); + }; + + case Comparison::CONTAINS: + return function ($object) use ($field, $value) { + return false !== strpos(static::getObjectFieldValue($object, $field), $value); + }; + + case Comparison::MEMBER_OF: + return function ($object) use ($field, $value) { + $fieldValues = static::getObjectFieldValue($object, $field); + if (!is_array($fieldValues)) { + $fieldValues = iterator_to_array($fieldValues); + } + return \in_array($value, $fieldValues, true); + }; + + case Comparison::STARTS_WITH: + return function ($object) use ($field, $value) { + return 0 === strpos(static::getObjectFieldValue($object, $field), $value); + }; + + case Comparison::ENDS_WITH: + return function ($object) use ($field, $value) { + return $value === substr(static::getObjectFieldValue($object, $field), -strlen($value)); + }; + + + default: + throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator()); + } + } +} diff --git a/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php b/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php index 199cbc3d6b..48879139f5 100644 --- a/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php +++ b/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php @@ -8,13 +8,14 @@ namespace Grav\Framework\Object\Interfaces; +use Doctrine\Common\Collections\Selectable; use Grav\Framework\Collection\CollectionInterface; /** * ObjectCollection Interface * @package Grav\Framework\Collection */ -interface ObjectCollectionInterface extends CollectionInterface, ObjectInterface +interface ObjectCollectionInterface extends CollectionInterface, Selectable, ObjectInterface { /** * Create a copy from this collection by cloning all objects in the collection. diff --git a/system/src/Grav/Framework/Object/ObjectCollection.php b/system/src/Grav/Framework/Object/ObjectCollection.php index a637bf1e00..1d0ff30ce1 100644 --- a/system/src/Grav/Framework/Object/ObjectCollection.php +++ b/system/src/Grav/Framework/Object/ObjectCollection.php @@ -8,9 +8,11 @@ namespace Grav\Framework\Object; +use Doctrine\Common\Collections\Criteria; use Grav\Framework\Collection\ArrayCollection; use Grav\Framework\Object\Access\NestedPropertyCollectionTrait; use Grav\Framework\Object\Base\ObjectCollectionTrait; +use Grav\Framework\Object\Collection\ObjectExpressionVisitor; use Grav\Framework\Object\Interfaces\NestedObjectInterface; use Grav\Framework\Object\Interfaces\ObjectCollectionInterface; @@ -36,6 +38,39 @@ public function __construct(array $elements = [], $key = null) $this->setKey($key); } + /** + * {@inheritDoc} + */ + public function matching(Criteria $criteria) + { + $expr = $criteria->getWhereExpression(); + $filtered = $this->getElements(); + + if ($expr) { + $visitor = new ObjectExpressionVisitor(); + $filter = $visitor->dispatch($expr); + $filtered = array_filter($filtered, $filter); + } + + if ($orderings = $criteria->getOrderings()) { + $next = null; + foreach (array_reverse($orderings) as $field => $ordering) { + $next = ObjectExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next); + } + + uasort($filtered, $next); + } + + $offset = $criteria->getFirstResult(); + $length = $criteria->getMaxResults(); + + if ($offset || $length) { + $filtered = array_slice($filtered, (int)$offset, $length); + } + + return $this->createFrom($filtered); + } + protected function getElements() { return $this->toArray(); diff --git a/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php index c26903f216..e330a330cc 100644 --- a/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php @@ -95,10 +95,10 @@ protected function doHasProperty($property) } /** - * @param string $property Object property to be fetched. - * @param mixed $default Default value if property has not been set. - * @param bool $doCreate Set true to create variable. - * @return mixed Property value. + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param callable|bool $doCreate Set true to create variable. + * @return mixed Property value. */ protected function &doGetProperty($property, $default = null, $doCreate = false) { diff --git a/system/src/Grav/Framework/Route/Route.php b/system/src/Grav/Framework/Route/Route.php index 503d2a8ad5..63f6f7ec3c 100644 --- a/system/src/Grav/Framework/Route/Route.php +++ b/system/src/Grav/Framework/Route/Route.php @@ -178,7 +178,7 @@ public function getQueryParam($param) */ public function withGravParam($param, $value) { - return $this->withParam('gravParams', $param, $value); + return $this->withParam('gravParams', $param, null !== $value ? (string)$value : null); } /** @@ -222,17 +222,16 @@ public function __toString() protected function withParam($type, $param, $value) { $oldValue = isset($this->{$type}[$param]) ? $this->{$type}[$param] : null; - $newValue = null !== $value ? (string)$value : null; - if ($oldValue === $newValue) { + if ($oldValue === $value) { return $this; } $new = clone $this; - if ($newValue === null) { + if ($value === null) { unset($new->{$type}[$param]); } else { - $new->{$type}[$param] = $newValue; + $new->{$type}[$param] = $value; } return $new; diff --git a/system/src/Grav/Framework/Route/RouteFactory.php b/system/src/Grav/Framework/Route/RouteFactory.php index a406926cf7..830d7c75c4 100644 --- a/system/src/Grav/Framework/Route/RouteFactory.php +++ b/system/src/Grav/Framework/Route/RouteFactory.php @@ -28,6 +28,23 @@ public static function createFromParts($parts) return new Route($parts); } + public static function createFromString($path) + { + $path = ltrim($path, '/'); + $parts = [ + 'path' => $path, + 'query' => '', + 'query_params' => [], + 'grav' => [ + 'root' => self::$root, + 'language' => self::$language, + 'route' => $path, + 'params' => '' + ], + ]; + return new Route($parts); + } + public static function getRoot() { return self::$root; diff --git a/system/src/Grav/Framework/Session/Session.php b/system/src/Grav/Framework/Session/Session.php new file mode 100644 index 0000000000..3c2a417e12 --- /dev/null +++ b/system/src/Grav/Framework/Session/Session.php @@ -0,0 +1,340 @@ +isSessionStarted()) { + session_unset(); + session_destroy(); + } + + // Set default options. + $options += array( + 'cache_limiter' => 'nocache', + 'use_trans_sid' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1 + ); + + $this->setOptions($options); + + session_register_shutdown(); + + self::$instance = $this; + } + + /** + * @inheritdoc + */ + public function getId() + { + return session_id(); + } + + /** + * @inheritdoc + */ + public function setId($id) + { + session_id($id); + + return $this; + } + + /** + * @inheritdoc + */ + public function getName() + { + return session_name(); + } + + /** + * @inheritdoc + */ + public function setName($name) + { + session_name($name); + + return $this; + } + + /** + * @inheritdoc + */ + public function setOptions(array $options) + { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $allowedOptions = [ + 'save_path' => true, + 'name' => true, + 'save_handler' => true, + 'gc_probability' => true, + 'gc_divisor' => true, + 'gc_maxlifetime' => true, + 'serialize_handler' => true, + 'cookie_lifetime' => true, + 'cookie_path' => true, + 'cookie_domain' => true, + 'cookie_secure' => true, + 'cookie_httponly' => true, + 'use_strict_mode' => true, + 'use_cookies' => true, + 'use_only_cookies' => true, + 'referer_check' => true, + 'cache_limiter' => true, + 'cache_expire' => true, + 'use_trans_sid' => true, + 'trans_sid_tags' => true, // PHP 7.1 + 'trans_sid_hosts' => true, // PHP 7.1 + 'sid_length' => true, // PHP 7.1 + 'sid_bits_per_character' => true, // PHP 7.1 + 'upload_progress.enabled' => true, + 'upload_progress.cleanup' => true, + 'upload_progress.prefix' => true, + 'upload_progress.name' => true, + 'upload_progress.freq' => true, + 'upload_progress.min-freq' => true, + 'lazy_write' => true, + 'url_rewriter.tags' => true, // Not used in PHP 7.1 + 'hash_function' => true, // Not used in PHP 7.1 + 'hash_bits_per_character' => true, // Not used in PHP 7.1 + 'entropy_file' => true, // Not used in PHP 7.1 + 'entropy_length' => true, // Not used in PHP 7.1 + ]; + + foreach ($options as $key => $value) { + if (is_array($value)) { + // Allow nested options. + foreach ($value as $key2 => $value2) { + $ckey = "{$key}.{$key2}"; + if (isset($value2, $allowedOptions[$ckey])) { + $this->ini_set("session.{$ckey}", $value2); + } + } + } elseif (isset($value, $allowedOptions[$key])) { + $this->ini_set("session.{$key}", $value); + } + } + } + + /** + * @inheritdoc + */ + public function start($readonly = false) + { + // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836 + if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) { + unset($_COOKIE[session_name()]); + } + + $options = $readonly ? ['read_and_close' => '1'] : []; + + $success = @session_start($options); + if (!$success) { + $last = error_get_last(); + $error = $last ? $last['message'] : 'Unknown error'; + throw new \RuntimeException('Failed to start session: ' . $error, 500); + } + + $params = session_get_cookie_params(); + + setcookie( + session_name(), + session_id(), + time() + $params['lifetime'], + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + + $this->started = true; + + return $this; + } + + /** + * @inheritdoc + */ + public function invalidate() + { + $params = session_get_cookie_params(); + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + + session_unset(); + session_destroy(); + + $this->started = false; + + return $this; + } + + /** + * @inheritdoc + */ + public function close() + { + if ($this->started) { + session_write_close(); + } + + $this->started = false; + + return $this; + } + + /** + * @inheritdoc + */ + public function clear() + { + session_unset(); + + return $this; + } + + /** + * @inheritdoc + */ + public function getAll() + { + return $_SESSION; + } + + /** + * @inheritdoc + */ + public function getIterator() + { + return new \ArrayIterator($_SESSION); + } + + /** + * @inheritdoc + */ + public function isStarted() + { + return $this->started; + } + + /** + * @inheritdoc + */ + public function __isset($name) + { + return isset($_SESSION[$name]); + } + + /** + * @inheritdoc + */ + public function __get($name) + { + return isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + + /** + * @inheritdoc + */ + public function __set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * @inheritdoc + */ + public function __unset($name) + { + unset($_SESSION[$name]); + } + + /** + * http://php.net/manual/en/function.session-status.php#113468 + * Check if session is started nicely. + * @return bool + */ + protected function isSessionStarted() + { + return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false; + } + + /** + * @param string $key + * @param mixed $value + */ + protected function ini_set($key, $value) + { + if (!is_string($value)) { + if (is_bool($value)) { + $value = $value ? '1' : '0'; + } + $value = (string)$value; + } + + ini_set($key, $value); + } +} diff --git a/system/src/Grav/Framework/Session/SessionInterface.php b/system/src/Grav/Framework/Session/SessionInterface.php new file mode 100644 index 0000000000..74c5f2aca4 --- /dev/null +++ b/system/src/Grav/Framework/Session/SessionInterface.php @@ -0,0 +1,147 @@ +assertEquals('engli' . '...', Utils::truncate('english', 5, true, " ", "...")); $this->assertEquals('english', Utils::truncate('english')); $this->assertEquals('This is a string to truncate', Utils::truncate('This is a string to truncate')); - $this->assertEquals('This ', Utils::truncate('This is a string to truncate', 3, true)); - $this->assertEquals('', 6, true)); + $this->assertEquals('This' . '…', Utils::truncate('This is a string to truncate', 3, true)); + $this->assertEquals('', 6, true)); } public function testSafeTruncate() { - $this->assertEquals('This ', Utils::safeTruncate('This is a string to truncate', 1)); - $this->assertEquals('This ', Utils::safeTruncate('This is a string to truncate', 4)); - $this->assertEquals('This is ', Utils::safeTruncate('This is a string to truncate', 5)); + $this->assertEquals('This' . '…', Utils::safeTruncate('This is a string to truncate', 1)); + $this->assertEquals('This' . '…', Utils::safeTruncate('This is a string to truncate', 4)); + $this->assertEquals('This is' . '…', Utils::safeTruncate('This is a string to truncate', 5)); } public function testTruncateHtml() diff --git a/user/pages/02.typography/default.md b/user/pages/02.typography/default.md index 88691f6bbc..d3aa848509 100644 --- a/user/pages/02.typography/default.md +++ b/user/pages/02.typography/default.md @@ -53,7 +53,7 @@ _Italic_ `_Italic_` TextSuperscripted `` -TextSubscxripted `` +TextSubscripted `` Underlined `` diff --git a/webserver-configs/nginx-ddev-site.conf b/webserver-configs/nginx-ddev-site.conf new file mode 100644 index 0000000000..e75cf2ff11 --- /dev/null +++ b/webserver-configs/nginx-ddev-site.conf @@ -0,0 +1,118 @@ +# ddev GravCMS config + +# You can override ddev's configuration by placing an edited copy +# of this config (or one of the other ones) in .ddev/nginx-site.conf +# See https://ddev.readthedocs.io/en/latest/users/extend/customization-extendibility/#providing-custom-nginx-configuration + +# Set https to 'on' if x-forwarded-proto is https +map $http_x_forwarded_proto $fcgi_https { + default off; + https on; +} + +server { + listen 80; ## listen for ipv4; this line is default and implied + listen [::]:80 default ipv6only=on; ## listen for ipv6 + # The NGINX_DOCROOT variable is substituted with + # its value when the container is started. + root $NGINX_DOCROOT; + index index.php index.htm index.html; + + # Make site accessible from http://localhost/ + server_name _; + + # Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html + sendfile off; + error_log /var/log/nginx/error.log info; + access_log /var/log/nginx/access.log; + + location / { + absolute_redirect off; + try_files $uri $uri/ /index.php?$query_string; + } + + # pass the PHP scripts to FastCGI server listening on socket + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_intercept_errors on; + # fastcgi_read_timeout should match max_execution_time in php.ini + fastcgi_read_timeout 10m; + fastcgi_param SERVER_NAME $host; + fastcgi_param HTTPS $fcgi_https; + } + + # Expire rules for static content + # Feed + location ~* \.(?:rss|atom|cache)$ { + expires 1h; + } + + # Media: images, icons, video, audio, HTC + location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { + expires 1M; + access_log off; + add_header Cache-Control "public"; + } + + # Prevent clients from accessing hidden files (starting with a dot) + # This is particularly important if you store .htpasswd files in the site hierarchy + # Access to `/.well-known/` is allowed. + # https://www.mnot.net/blog/2010/04/07/well-known + # https://tools.ietf.org/html/rfc5785 + location ~* /\.(?!well-known\/) { + deny all; + } + + # Prevent clients from accessing to backup/config/source files + location ~* (?:\.(?:bak|conf|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$ { + deny all; + } + + ## Begin - Security + # deny all direct access for these folders + location ~* /(\.git|cache|bin|logs|backup|tests)/.*$ { return 403; } + # deny running scripts inside core system folders + location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 403; } + # deny running scripts inside user folder + location ~* /user/.*\.(txt|md|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 403; } + # deny access to specific files in the root folder + location ~ /(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 403; } + ## End - Security + + + ## provide a health check endpoint + location /healthcheck { + access_log off; + stub_status on; + keepalive_timeout 0; # Disable HTTP keepalive + return 200; + } + + error_page 400 401 /40x.html; + location = /40x.html { + root /usr/share/nginx/html; + } + + location ~ ^/(fpmstatus|ping)$ { + access_log off; + stub_status on; + keepalive_timeout 0; # Disable HTTP keepalive + allow 127.0.0.1; + allow all; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass unix:/run/php-fpm.sock; + } + + +} + diff --git a/webserver-configs/nginx.conf b/webserver-configs/nginx.conf index 89dd1051e4..bbca7d32db 100644 --- a/webserver-configs/nginx.conf +++ b/webserver-configs/nginx.conf @@ -30,7 +30,7 @@ server { ## Begin - PHP location ~ \.php$ { # Choose either a socket or TCP/IP address - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; + fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # fastcgi_pass unix:/var/run/php5-fpm.sock; #legacy # fastcgi_pass 127.0.0.1:9000;