diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e84f52bf5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: grav +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fb45e09..bb00c69ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# v1.6.10 +## 06/14/2019 + +1. [](#improved) + * Added **page blueprints** to `YamlLinter` CLI and Admin reports + * Removed `Gitter` and `Slack` [#2502](https://github.com/getgrav/grav/issues/2502) + * Optimizations for Plugin/Theme loading + * Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance + * Change minimal port number to 0 (unix socket) [#2452](https://github.com/getgrav/grav/issues/2452) +1. [](#bugfix) + * Force question to install demo content in theme update [#2493](https://github.com/getgrav/grav/issues/2493) + * Fixed GPM errors from blueprints not being logged [#2505](https://github.com/getgrav/grav/issues/2505) + * Don't error when IP is invalid [#2507](https://github.com/getgrav/grav/issues/2507) + * Fixed regression with `bin/plugin` not listing the plugins available (1c725c0) + * Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518) + * Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511) + * Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511) + * Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524) + # v1.6.9 ## 05/09/2019 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da8896f69..112fe4d4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ The issue tracker is the preferred channel for [bug reports](#bugs), requests](#pull-requests), but please respect the following restrictions: * Please **do not** use the issue tracker for support requests. Use - [the Forum](http://getgrav.org/forum) or [the Gitter chat](https://gitter.im/getgrav/grav). + [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/). @@ -110,7 +110,8 @@ Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. -**Please ask first** in [Slack](https://getgrav.org/slack) or in the Forum before embarking on any significant pull request (e.g. +**Please ask first** in [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/) +before embarking on any significant pull request (e.g. implementing features, refactoring code..), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. diff --git a/bin/plugin b/bin/plugin index 6f8466d81..3c431e379 100755 --- a/bin/plugin +++ b/bin/plugin @@ -79,19 +79,6 @@ $output = new ConsoleOutput(); $output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold'))); $output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold'))); -if (is_null($plugin)) { - $output->writeln(''); - $output->writeln("$name plugin not found"); - die; -} - -if (!$plugin->enabled) { - $output->writeln(''); - $output->writeln("$name not enabled"); - die; -} - - if (!$name) { $output->writeln(''); $output->writeln('Usage:'); @@ -123,6 +110,18 @@ if (!$name) { } exit; +} else { + if (is_null($plugin)) { + $output->writeln(''); + $output->writeln("$name plugin not found"); + die; + } + + if (!$plugin->enabled) { + $output->writeln(''); + $output->writeln("$name not enabled"); + die; + } } if ($plugin === null) { diff --git a/composer.lock b/composer.lock index 5c247be35..be6bf65b6 100644 --- a/composer.lock +++ b/composer.lock @@ -1846,16 +1846,16 @@ }, { "name": "symfony/contracts", - "version": "v1.0.2", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/symfony/contracts.git", - "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf" + "reference": "d3636025e8253c6144358ec0a62773cae588395b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/contracts/zipball/1aa7ab2429c3d594dd70689604b5cf7421254cdf", - "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf", + "url": "https://api.github.com/repos/symfony/contracts/zipball/d3636025e8253c6144358ec0a62773cae588395b", + "reference": "d3636025e8253c6144358ec0a62773cae588395b", "shasum": "" }, "require": { @@ -1863,19 +1863,22 @@ }, "require-dev": { "psr/cache": "^1.0", - "psr/container": "^1.0" + "psr/container": "^1.0", + "symfony/polyfill-intl-idn": "^1.10" }, "suggest": { "psr/cache": "When using the Cache contracts", "psr/container": "When using the Service contracts", "symfony/cache-contracts-implementation": "", + "symfony/event-dispatcher-implementation": "", + "symfony/http-client-contracts-implementation": "", "symfony/service-contracts-implementation": "", "symfony/translation-contracts-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -1910,7 +1913,7 @@ "interoperability", "standards" ], - "time": "2018-12-05T08:06:11+00:00" + "time": "2019-04-27T14:29:50+00:00" }, { "name": "symfony/event-dispatcher", @@ -2451,16 +2454,16 @@ }, { "name": "twig/twig", - "version": "v1.40.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "35889516bbd6bbe46a600c2c33b03515df4a076e" + "reference": "575cd5028362da591facde1ef5d7b94553c375c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/35889516bbd6bbe46a600c2c33b03515df4a076e", - "reference": "35889516bbd6bbe46a600c2c33b03515df4a076e", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/575cd5028362da591facde1ef5d7b94553c375c9", + "reference": "575cd5028362da591facde1ef5d7b94553c375c9", "shasum": "" }, "require": { @@ -2475,7 +2478,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.40-dev" + "dev-master": "1.41-dev" } }, "autoload": { @@ -2513,7 +2516,7 @@ "keywords": [ "templating" ], - "time": "2019-04-29T14:12:28+00:00" + "time": "2019-05-14T11:59:08+00:00" }, { "name": "willdurand/negotiation", @@ -4208,16 +4211,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.11.5", + "version": "0.11.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "24ce5a566a798b81343138ed5d41d6877554cf9a" + "reference": "7af8b9d02b3ab36444dbf4e1b9ca1c1bd5044d81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/24ce5a566a798b81343138ed5d41d6877554cf9a", - "reference": "24ce5a566a798b81343138ed5d41d6877554cf9a", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7af8b9d02b3ab36444dbf4e1b9ca1c1bd5044d81", + "reference": "7af8b9d02b3ab36444dbf4e1b9ca1c1bd5044d81", "shasum": "" }, "require": { @@ -4277,7 +4280,7 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "time": "2019-03-25T16:40:09+00:00" + "time": "2019-05-08T16:33:56+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -4579,16 +4582,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.5.9", + "version": "7.5.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160" + "reference": "64cb33f5b520da490a7b13149d39b43cf3c890c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/134669cf0eeac3f79bc7f0c793efbc158bffc160", - "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/64cb33f5b520da490a7b13149d39b43cf3c890c6", + "reference": "64cb33f5b520da490a7b13149d39b43cf3c890c6", "shasum": "" }, "require": { @@ -4659,7 +4662,7 @@ "testing", "xunit" ], - "time": "2019-04-19T15:50:46+00:00" + "time": "2019-05-14T04:53:02+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -4828,16 +4831,16 @@ }, { "name": "sebastian/environment", - "version": "4.2.1", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a" + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/3095910f0f0fb155ac4021fc51a4a7a39ac04e8a", - "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", "shasum": "" }, "require": { @@ -4877,7 +4880,7 @@ "environment", "hhvm" ], - "time": "2019-04-25T07:55:20+00:00" + "time": "2019-05-05T09:05:15+00:00" }, { "name": "sebastian/exporter", diff --git a/now.json b/now.json new file mode 100644 index 000000000..fe7b94b37 --- /dev/null +++ b/now.json @@ -0,0 +1,4 @@ + { + "version": 2, + "builds": [{ "src": "*.php", "use": "@now/php" }] +} diff --git a/system/blueprints/config/site.yaml b/system/blueprints/config/site.yaml index 57fd3c57a..660382347 100644 --- a/system/blueprints/config/site.yaml +++ b/system/blueprints/config/site.yaml @@ -65,7 +65,7 @@ form: summary.size: type: text - size: x-small + size: small append: PLUGIN_ADMIN.CHARACTERS label: PLUGIN_ADMIN.SUMMARY_SIZE help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP diff --git a/system/defines.php b/system/defines.php index 7a4df5cce..405b68062 100644 --- a/system/defines.php +++ b/system/defines.php @@ -8,7 +8,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.6.9'); +define('GRAV_VERSION', '1.6.10'); define('GRAV_TESTING', false); define('DS', '/'); diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php index 0b0719d09..1d0fcc689 100644 --- a/system/src/Grav/Common/Data/Blueprints.php +++ b/system/src/Grav/Common/Data/Blueprints.php @@ -39,7 +39,8 @@ class Blueprints public function get($type) { if (!isset($this->instances[$type])) { - $this->instances[$type] = $this->loadFile($type); + $blueprint = $this->loadFile($type); + $this->instances[$type] = $blueprint; } return $this->instances[$type]; @@ -99,6 +100,15 @@ class Blueprints $blueprint->setContext($this->search); } - return $blueprint->load()->init(); + try { + $blueprint->load()->init(); + } catch (\RuntimeException $e) { + $log = Grav::instance()['log']; + $log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage())); + + throw $e; + } + + return $blueprint; } } diff --git a/system/src/Grav/Common/GPM/Common/CachedCollection.php b/system/src/Grav/Common/GPM/Common/CachedCollection.php index 7c769752e..a6fca4db0 100644 --- a/system/src/Grav/Common/GPM/Common/CachedCollection.php +++ b/system/src/Grav/Common/GPM/Common/CachedCollection.php @@ -11,19 +11,22 @@ namespace Grav\Common\GPM\Common; use Grav\Common\Iterator; -class CachedCollection extends Iterator { - +class CachedCollection extends Iterator +{ protected static $cache; public function __construct($items) { parent::__construct(); + + $method = static::class . __METHOD__; + // local cache to speed things up - if (!isset(self::$cache[get_called_class() . __METHOD__])) { - self::$cache[get_called_class() . __METHOD__] = $items; + if (!isset(self::$cache[$method])) { + self::$cache[$method] = $items; } - foreach (self::$cache[get_called_class() . __METHOD__] as $name => $item) { + foreach (self::$cache[$method] as $name => $item) { $this->append([$name => $item]); } } diff --git a/system/src/Grav/Common/GPM/Common/Package.php b/system/src/Grav/Common/GPM/Common/Package.php index d205f1ee1..40d375912 100644 --- a/system/src/Grav/Common/GPM/Common/Package.php +++ b/system/src/Grav/Common/GPM/Common/Package.php @@ -11,8 +11,8 @@ namespace Grav\Common\GPM\Common; use Grav\Common\Data\Data; -class Package { - +class Package +{ /** * @var Data */ diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 94eec7d99..6c9e7b0e4 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -770,7 +770,7 @@ class GPM extends Iterator * @param array $ignore_packages_list * * @return bool - * @throws \Exception + * @throws \RuntimeException */ public function checkNoOtherPackageNeedsThisDependencyInALowerVersion( $slug, @@ -793,8 +793,8 @@ class GPM extends Iterator $compatible = $this->checkNextSignificantReleasesAreCompatible($version, $other_dependency_version); if (!$compatible) { - if (!in_array($dependent_package, $ignore_packages_list)) { - throw new \Exception("Package $slug is required in an older version by package $dependent_package. This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug.", + if (!in_array($dependent_package, $ignore_packages_list, true)) { + throw new \RuntimeException("Package $slug is required in an older version by package $dependent_package. This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug.", 2); } } @@ -850,10 +850,10 @@ class GPM extends Iterator ) { //Needs a Grav update first throw new \RuntimeException("One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this"); - } else { - unset($dependencies[$dependency_slug]); - continue; } + + unset($dependencies[$dependency_slug]); + continue; } //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell. @@ -863,10 +863,10 @@ class GPM extends Iterator ) { //Needs a Grav update first throw new \RuntimeException("One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release."); - } else { - unset($dependencies[$dependency_slug]); - continue; } + + unset($dependencies[$dependency_slug]); + continue; } if ($this->isPluginInstalled($dependency_slug)) { @@ -1092,6 +1092,7 @@ class GPM extends Iterator if ($this->versionFormatIsEqualOrHigher($version)) { return trim(substr($version, 2)); } + return $version; } @@ -1104,7 +1105,7 @@ class GPM extends Iterator * * @return bool */ - public function versionFormatIsNextSignificantRelease($version) + public function versionFormatIsNextSignificantRelease($version): bool { return strpos($version, '~') === 0; } @@ -1118,7 +1119,7 @@ class GPM extends Iterator * * @return bool */ - public function versionFormatIsEqualOrHigher($version) + public function versionFormatIsEqualOrHigher($version): bool { return strpos($version, '>=') === 0; } @@ -1136,7 +1137,7 @@ class GPM extends Iterator * * @return bool */ - public function checkNextSignificantReleasesAreCompatible($version1, $version2) + public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool { $version1array = explode('.', $version1); $version2array = explode('.', $version2); diff --git a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php index 9acc08385..be565c9c9 100644 --- a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php +++ b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php @@ -16,6 +16,7 @@ abstract class AbstractPackageCollection extends BaseCollection public function __construct($items) { parent::__construct(); + foreach ($items as $name => $data) { $data->set('slug', $name); $this->items[$name] = new Package($data, $this->type); diff --git a/system/src/Grav/Common/GPM/Local/Plugins.php b/system/src/Grav/Common/GPM/Local/Plugins.php index 9c8e6dd13..19adfb88e 100644 --- a/system/src/Grav/Common/GPM/Local/Plugins.php +++ b/system/src/Grav/Common/GPM/Local/Plugins.php @@ -25,6 +25,7 @@ class Plugins extends AbstractPackageCollection { /** @var \Grav\Common\Plugins $plugins */ $plugins = Grav::instance()['plugins']; + parent::__construct($plugins->all()); } } diff --git a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php index b46dc04da..429c5b1dc 100644 --- a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php +++ b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php @@ -43,7 +43,7 @@ class AbstractPackageCollection extends BaseCollection { parent::__construct(); if ($repository === null) { - throw new \RuntimeException("A repository is required to indicate the origin of the remote collection"); + throw new \RuntimeException('A repository is required to indicate the origin of the remote collection'); } $channel = Grav::instance()['config']->get('system.gpm.releases', 'stable'); diff --git a/system/src/Grav/Common/GPM/Remote/GravCore.php b/system/src/Grav/Common/GPM/Remote/GravCore.php index 168f82f8e..1d30120d0 100644 --- a/system/src/Grav/Common/GPM/Remote/GravCore.php +++ b/system/src/Grav/Common/GPM/Remote/GravCore.php @@ -37,9 +37,9 @@ class GravCore extends AbstractPackageCollection $this->fetch($refresh, $callback); $this->data = json_decode($this->raw, true); - $this->version = isset($this->data['version']) ? $this->data['version'] : '-'; - $this->date = isset($this->data['date']) ? $this->data['date'] : '-'; - $this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null; + $this->version = $this->data['version'] ?? '-'; + $this->date = $this->data['date'] ?? '-'; + $this->min_php = $this->data['min_php'] ?? null; if (isset($this->data['assets'])) { foreach ((array)$this->data['assets'] as $slug => $data) { diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php index 094324213..f07c3aa5a 100644 --- a/system/src/Grav/Common/Helpers/Excerpts.php +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -9,24 +9,20 @@ namespace Grav\Common\Helpers; -use Grav\Common\Grav; use Grav\Common\Page\Interfaces\PageInterface; -use Grav\Common\Uri; +use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject; use Grav\Common\Page\Medium\Medium; -use Grav\Common\Utils; -use RocketTheme\Toolbox\Event\Event; -use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; class Excerpts { /** * Process Grav image media URL from HTML tag * - * @param string $html HTML tag e.g. `` - * @param PageInterface $page The current page object - * @return string Returns final HTML string + * @param string $html HTML tag e.g. `` + * @param PageInterface|null $page Page, defaults to the current page object + * @return string Returns final HTML string */ - public static function processImageHtml($html, PageInterface $page) + public static function processImageHtml($html, PageInterface $page = null) { $excerpt = static::getExcerptFromHtml($html, 'img'); @@ -112,157 +108,29 @@ class Excerpts * Process a Link excerpt * * @param array $excerpt - * @param PageInterface $page + * @param PageInterface|null $page Page, defaults to the current page object * @param string $type * @return mixed */ - public static function processLinkExcerpt($excerpt, PageInterface $page, $type = 'link') + public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link') { - $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + $excerpts = new ExcerptsObject($page); - $url_parts = static::parseUrl($url); - - // If there is a query, then parse it and build action calls. - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; - $carry[$parts[0]] = $value; - - return $carry; - }, []); - - // Valid attributes supported. - $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; - - // Unless told to not process, go through actions. - if (array_key_exists('noprocess', $actions)) { - unset($actions['noprocess']); - } else { - // Loop through actions for the image and call them. - foreach ($actions as $attrib => $value) { - $key = $attrib; - - if (in_array($attrib, $valid_attributes, true)) { - // support both class and classes. - if ($attrib === 'classes') { - $attrib = 'class'; - } - $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); - unset($actions[$key]); - } - } - } - - $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); - } - - // If no query elements left, unset query. - if (empty($url_parts['query'])) { - unset ($url_parts['query']); - } - - // Set path to / if not set. - if (empty($url_parts['path'])) { - $url_parts['path'] = ''; - } - - // If scheme isn't http(s).. - if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { - // Handle custom streams. - if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { - $url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); - unset($url_parts['stream'], $url_parts['scheme']); - } - - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - return $excerpt; - } - - // Handle paths and such. - $url_parts = Uri::convertUrl($page, $url_parts, $type); - - // Build the URL from the component parts and set it on the element. - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - - return $excerpt; + return $excerpts->processLinkExcerpt($excerpt, $type); } /** * Process an image excerpt * * @param array $excerpt - * @param PageInterface $page + * @param PageInterface|null $page Page, defaults to the current page object * @return array */ - public static function processImageExcerpt(array $excerpt, PageInterface $page) + public static function processImageExcerpt(array $excerpt, PageInterface $page = null) { - $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); - $url_parts = static::parseUrl($url); + $excerpts = new ExcerptsObject($page); - $media = null; - $filename = null; - - if (!empty($url_parts['stream'])) { - $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); - - $media = $page->getMedia(); - - } else { - $grav = Grav::instance(); - - // File is also local if scheme is http(s) and host matches. - $local_file = isset($url_parts['path']) - && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) - && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); - - if ($local_file) { - $filename = basename($url_parts['path']); - $folder = dirname($url_parts['path']); - - // Get the local path to page media if possible. - if ($folder === $page->url(false, false, false)) { - // Get the media objects for this page. - $media = $page->getMedia(); - } else { - // see if this is an external page to this one - $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/'); - $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); - - /** @var PageInterface $ext_page */ - $ext_page = $grav['pages']->dispatch($page_route, true); - if ($ext_page) { - $media = $ext_page->getMedia(); - } else { - $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); - } - } - } - } - - // If there is a media file that matches the path referenced.. - if ($media && $filename && isset($media[$filename])) { - // Get the medium object. - /** @var Medium $medium */ - $medium = $media[$filename]; - - // Process operations - $medium = static::processMediaActions($medium, $url_parts); - $element_excerpt = $excerpt['element']['attributes']; - - $alt = $element_excerpt['alt'] ?? ''; - $title = $element_excerpt['title'] ?? ''; - $class = $element_excerpt['class'] ?? ''; - $id = $element_excerpt['id'] ?? ''; - - $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); - - } else { - // Not a current page media file, see if it needs converting to relative. - $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); - } - - return $excerpt; + return $excerpts->processImageExcerpt($excerpt); } /** @@ -270,104 +138,13 @@ class Excerpts * * @param Medium $medium * @param string|array $url + * @param PageInterface|null $page Page, defaults to the current page object * @return Medium */ - public static function processMediaActions($medium, $url) + public static function processMediaActions($medium, $url, PageInterface $page = null) { - if (!is_array($url)) { - $url_parts = parse_url($url); - } else { - $url_parts = $url; - } + $excerpts = new ExcerptsObject($page); - $actions = []; - - // if there is a query, then parse it and build action calls - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = $parts[1] ?? null; - $carry[] = ['method' => $parts[0], 'params' => $value]; - - return $carry; - }, []); - } - - if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) { - $actions[] = ['method' => 'fixOrientation', 'params' => '']; - } - $defaults = Grav::instance()['config']->get('system.images.defaults'); - if (is_array($defaults) && count($defaults)) { - foreach ($defaults as $method => $params) { - $actions[] = [ - 'method' => $method, - 'params' => $params, - ]; - } - } - - // loop through actions for the image and call them - foreach ($actions as $action) { - $matches = []; - - if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { - $args = [explode(',', $matches[1])]; - } else { - $args = explode(',', $action['params']); - } - - $medium = call_user_func_array([$medium, $action['method']], $args); - } - - if (isset($url_parts['fragment'])) { - $medium->urlHash($url_parts['fragment']); - } - - return $medium; - } - - /** - * Variation of parse_url() which works also with local streams. - * - * @param string $url - * @return array|bool - */ - protected static function parseUrl($url) - { - $url_parts = Utils::multibyteParseUrl($url); - - if (isset($url_parts['scheme'])) { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - // Special handling for the streams. - if ($locator->schemeExists($url_parts['scheme'])) { - if (isset($url_parts['host'])) { - // Merge host and path into a path. - $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); - unset($url_parts['host']); - } - - $url_parts['stream'] = true; - } - } - - return $url_parts; - } - - /** - * @param string $url - * @return bool|string - */ - protected static function resolveStream($url) - { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - if ($locator->isStream($url)) { - return $locator->findResource($url, false) ?: $locator->findResource($url, false, true); - } - - return $url; + return $excerpts->processMediaActions($medium, $url); } } diff --git a/system/src/Grav/Common/Helpers/YamlLinter.php b/system/src/Grav/Common/Helpers/YamlLinter.php index 43bde820b..b7e608d00 100644 --- a/system/src/Grav/Common/Helpers/YamlLinter.php +++ b/system/src/Grav/Common/Helpers/YamlLinter.php @@ -20,7 +20,8 @@ class YamlLinter { $errors = static::lintConfig(); $errors = $errors + static::lintPages(); - + $errors = $errors + static::lintBlueprints(); + return $errors; } @@ -34,6 +35,18 @@ class YamlLinter return static::recurseFolder('config://'); } + public static function lintBlueprints() + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + $current_theme = Grav::instance()['config']->get('system.pages.theme'); + $theme_path = 'themes://' . $current_theme . '/blueprints'; + + $locator->addPath('blueprints', '', [$theme_path]); + return static::recurseFolder('blueprints://'); + } + public static function recurseFolder($path, $extensions = 'md|yaml') { $lint_errors = []; diff --git a/system/src/Grav/Common/Markdown/Parsedown.php b/system/src/Grav/Common/Markdown/Parsedown.php index d2f72fcb7..32033ddff 100644 --- a/system/src/Grav/Common/Markdown/Parsedown.php +++ b/system/src/Grav/Common/Markdown/Parsedown.php @@ -10,6 +10,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Page\Markdown\Excerpts; class Parsedown extends \Parsedown { @@ -18,12 +19,21 @@ class Parsedown extends \Parsedown /** * Parsedown constructor. * - * @param PageInterface $page + * @param Excerpts|null $excerpts * @param array|null $defaults */ - public function __construct($page, $defaults) + public function __construct($excerpts = null, $defaults = null) { - $this->init($page, $defaults); + if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php index ac0477150..a562d8d4a 100644 --- a/system/src/Grav/Common/Markdown/ParsedownExtra.php +++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php @@ -10,6 +10,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Page\Markdown\Excerpts; class ParsedownExtra extends \ParsedownExtra { @@ -18,14 +19,23 @@ class ParsedownExtra extends \ParsedownExtra /** * ParsedownExtra constructor. * - * @param PageInterface $page + * @param Excerpts|null $excerpts * @param array|null $defaults * @throws \Exception */ - public function __construct($page, $defaults) + public function __construct($excerpts = null, $defaults = null) { + if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + parent::__construct(); - $this->init($page, $defaults); + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php index fa8fa9156..aa76cdf0c 100644 --- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -9,15 +9,13 @@ namespace Grav\Common\Markdown; -use Grav\Common\Grav; -use Grav\Common\Helpers\Excerpts; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Page\Interfaces\PageInterface; -use RocketTheme\Toolbox\Event\Event; trait ParsedownGravTrait { - /** @var PageInterface $page */ - protected $page; + /** @var Excerpts */ + protected $excerpts; protected $special_chars; protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/'; @@ -28,28 +26,49 @@ trait ParsedownGravTrait /** * Initialization function to setup key variables needed by the MarkdownGravLinkTrait * - * @param PageInterface $page + * @param PageInterface|Excerpts|null $excerpts * @param array|null $defaults */ - protected function init($page, $defaults) + protected function init($excerpts = null, $defaults = null) { - $grav = Grav::instance(); - - $this->page = $page; - $this->BlockTypes['{'] [] = 'TwigTag'; - $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - - if ($defaults === null) { - $defaults = (array)Grav::instance()['config']->get('system.pages.markdown'); + if (!$excerpts || $excerpts instanceof PageInterface) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $this->excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } else { + $this->excerpts = $excerpts; } - $this->setBreaksEnabled($defaults['auto_line_breaks']); - $this->setUrlsLinked($defaults['auto_url_links']); - $this->setMarkupEscaped($defaults['escape_markup']); - $this->setSpecialChars($defaults['special_chars']); + $this->BlockTypes['{'][] = 'TwigTag'; + $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this, 'page' => $page])); + $defaults = $this->excerpts->getConfig(); + if (isset($defaults['markdown']['auto_line_breaks'])) { + $this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']); + } + if (isset($defaults['markdown']['auto_url_links'])) { + $this->setUrlsLinked($defaults['markdown']['auto_url_links']); + } + if (isset($defaults['markdown']['escape_markup'])) { + $this->setMarkupEscaped($defaults['markdown']['escape_markup']); + } + if (isset($defaults['markdown']['special_chars'])) { + $this->setSpecialChars($defaults['markdown']['special_chars']); + } + + $this->excerpts->fireInitializedEvent($this); + } + + /** + * @return Excerpts + */ + public function getExcerpts() + { + return $this->excerpts; } /** @@ -114,7 +133,8 @@ trait ParsedownGravTrait */ protected function isBlockContinuable($Type) { - $continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue'); + $continuable = \in_array($Type, $this->continuable_blocks, true) + || method_exists($this, 'block' . $Type . 'Continue'); return $continuable; } @@ -128,7 +148,8 @@ trait ParsedownGravTrait */ protected function isBlockCompletable($Type) { - $completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete'); + $completable = \in_array($Type, $this->completable_blocks, true) + || method_exists($this, 'block' . $Type . 'Complete'); return $completable; } @@ -210,7 +231,7 @@ trait ParsedownGravTrait // if this is an image process it if (isset($excerpt['element']['attributes']['src'])) { - $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page); + $excerpt = $this->excerpts->processImageExcerpt($excerpt); } return $excerpt; @@ -218,11 +239,7 @@ trait ParsedownGravTrait protected function inlineLink($excerpt) { - if (isset($excerpt['type'])) { - $type = $excerpt['type']; - } else { - $type = 'link'; - } + $type = $excerpt['type'] ?? 'link'; // do some trickery to get around Parsedown requirement for valid URL if its Twig in there if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { @@ -238,13 +255,15 @@ trait ParsedownGravTrait // if this is a link if (isset($excerpt['element']['attributes']['href'])) { - $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type); + $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type); } return $excerpt; } - // For extending this class via plugins + /** + * For extending this class via plugins + */ public function __call($method, $args) { if (isset($this->{$method}) === true) { diff --git a/system/src/Grav/Common/Page/Markdown/Excerpts.php b/system/src/Grav/Common/Page/Markdown/Excerpts.php new file mode 100644 index 000000000..eaa13cbe8 --- /dev/null +++ b/system/src/Grav/Common/Page/Markdown/Excerpts.php @@ -0,0 +1,329 @@ +page = $page ?? Grav::instance()['page'] ?? null; + + // Add defaults to the configuration. + if (null === $config || !isset($config['markdown'], $config['images'])) { + $c = Grav::instance()['config']; + $config = $config ?? []; + $config += [ + 'markdown' => $c->get('system.pages.markdown', []), + 'images' => $c->get('system.images', []) + ]; + } + + $this->config = $config; + } + + public function getPage(): PageInterface + { + return $this->page; + } + + public function getConfig(): array + { + return $this->config; + } + + public function fireInitializedEvent($markdown): void + { + $grav = Grav::instance(); + + $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page])); + } + + /** + * Process a Link excerpt + * + * @param array $excerpt + * @param string $type + * @return array + */ + public function processLinkExcerpt(array $excerpt, string $type = 'link'): array + { + $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + + $url_parts = $this->parseUrl($url); + + // If there is a query, then parse it and build action calls. + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; + $carry[$parts[0]] = $value; + + return $carry; + }, + [] + ); + + // Valid attributes supported. + $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; + + // Unless told to not process, go through actions. + if (array_key_exists('noprocess', $actions)) { + unset($actions['noprocess']); + } else { + // Loop through actions for the image and call them. + foreach ($actions as $attrib => $value) { + $key = $attrib; + + if (in_array($attrib, $valid_attributes, true)) { + // support both class and classes. + if ($attrib === 'classes') { + $attrib = 'class'; + } + $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); + unset($actions[$key]); + } + } + } + + $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); + } + + // If no query elements left, unset query. + if (empty($url_parts['query'])) { + unset ($url_parts['query']); + } + + // Set path to / if not set. + if (empty($url_parts['path'])) { + $url_parts['path'] = ''; + } + + // If scheme isn't http(s).. + if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { + // Handle custom streams. + if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { + $grav = Grav::instance(); + $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); + unset($url_parts['stream'], $url_parts['scheme']); + } + + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + // Handle paths and such. + $url_parts = Uri::convertUrl($this->page, $url_parts, $type); + + // Build the URL from the component parts and set it on the element. + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + /** + * Process an image excerpt + * + * @param array $excerpt + * @return array + */ + public function processImageExcerpt(array $excerpt): array + { + $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); + $url_parts = $this->parseUrl($url); + + $media = null; + $filename = null; + + if (!empty($url_parts['stream'])) { + $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); + + $media = $this->page->getMedia(); + + } else { + $grav = Grav::instance(); + + // File is also local if scheme is http(s) and host matches. + $local_file = isset($url_parts['path']) + && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) + && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); + + if ($local_file) { + $filename = basename($url_parts['path']); + $folder = dirname($url_parts['path']); + + // Get the local path to page media if possible. + if ($this->page && $folder === $this->page->url(false, false, false)) { + // Get the media objects for this page. + $media = $this->page->getMedia(); + } else { + // see if this is an external page to this one + $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/'); + $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); + + /** @var PageInterface $ext_page */ + $ext_page = $grav['pages']->dispatch($page_route, true); + if ($ext_page) { + $media = $ext_page->getMedia(); + } else { + $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); + } + } + } + } + + // If there is a media file that matches the path referenced.. + if ($media && $filename && isset($media[$filename])) { + // Get the medium object. + /** @var Medium $medium */ + $medium = $media[$filename]; + + // Process operations + $medium = $this->processMediaActions($medium, $url_parts); + $element_excerpt = $excerpt['element']['attributes']; + + $alt = $element_excerpt['alt'] ?? ''; + $title = $element_excerpt['title'] ?? ''; + $class = $element_excerpt['class'] ?? ''; + $id = $element_excerpt['id'] ?? ''; + + $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); + + } else { + // Not a current page media file, see if it needs converting to relative. + $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); + } + + return $excerpt; + } + + /** + * Process media actions + * + * @param Medium $medium + * @param string|array $url + * @return Medium|Link + */ + public function processMediaActions($medium, $url) + { + $url_parts = is_string($url) ? $this->parseUrl($url) : $url; + $actions = []; + + // if there is a query, then parse it and build action calls + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = $parts[1] ?? null; + $carry[] = ['method' => $parts[0], 'params' => $value]; + + return $carry; + }, + [] + ); + } + + $config = $this->getConfig(); + if (!empty($config['images']['auto_fix_orientation'])) { + $actions[] = ['method' => 'fixOrientation', 'params' => '']; + } + + $defaults = $config['images']['defaults'] ?? []; + if (count($defaults)) { + foreach ($defaults as $method => $params) { + $actions[] = [ + 'method' => $method, + 'params' => $params, + ]; + } + } + + // loop through actions for the image and call them + foreach ($actions as $action) { + $matches = []; + + if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { + $args = [explode(',', $matches[1])]; + } else { + $args = explode(',', $action['params']); + } + + $medium = call_user_func_array([$medium, $action['method']], $args); + } + + if (isset($url_parts['fragment'])) { + $medium->urlHash($url_parts['fragment']); + } + + return $medium; + } + + /** + * Variation of parse_url() which works also with local streams. + * + * @param string $url + * @return array|bool + */ + protected function parseUrl(string $url) + { + $url_parts = Utils::multibyteParseUrl($url); + + if (isset($url_parts['scheme'])) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + // Special handling for the streams. + if ($locator->schemeExists($url_parts['scheme'])) { + if (isset($url_parts['host'])) { + // Merge host and path into a path. + $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); + unset($url_parts['host']); + } + + $url_parts['stream'] = true; + } + } + + return $url_parts; + } + + /** + * @param string $url + * @return bool|string + */ + protected function resolveStream(string $url) + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if ($locator->isStream($url)) { + return $locator->findResource($url, false) ?: $locator->findResource($url, false, true); + } + + return $url; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php index 627c361ff..c6d75bc77 100644 --- a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php +++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php @@ -10,6 +10,7 @@ namespace Grav\Common\Page\Medium; use Grav\Common\Markdown\Parsedown; +use Grav\Common\Page\Markdown\Excerpts; trait ParsedownHtmlTrait { @@ -33,7 +34,7 @@ trait ParsedownHtmlTrait $element = $this->parsedownElement($title, $alt, $class, $id, $reset); if (!$this->parsedown) { - $this->parsedown = new Parsedown(null, null); + $this->parsedown = new Parsedown(new Excerpts()); } return $this->parsedown->elementToHtml($element); diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 82ff71cb2..bcf7539bd 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -19,6 +19,7 @@ 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\Page\Markdown\Excerpts; use Grav\Common\Taxonomy; use Grav\Common\Uri; use Grav\Common\Utils; @@ -27,7 +28,6 @@ use Negotiation\Accept; use Negotiation\Negotiator; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\File\MarkdownFile; -use Symfony\Component\Yaml\Exception\ParseException; define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u'); @@ -819,23 +819,31 @@ class Page implements PageInterface /** @var Config $config */ $config = Grav::instance()['config']; - $defaults = (array)$config->get('system.pages.markdown'); + $markdownDefaults = (array)$config->get('system.pages.markdown'); if (isset($this->header()->markdown)) { - $defaults = array_merge($defaults, $this->header()->markdown); + $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown); } // pages.markdown_extra is deprecated, but still check it... - if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { + if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); - $defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); + $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); } + $extra = $markdownDefaults['extra'] ?? false; + $defaults = [ + 'markdown' => $markdownDefaults, + 'images' => $config->get('system.images', []) + ]; + + $excerpts = new Excerpts($this, $defaults); + // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($this, $defaults); + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); } else { - $parsedown = new Parsedown($this, $defaults); + $parsedown = new Parsedown($excerpts); } $this->content = $parsedown->text($this->content); @@ -1397,8 +1405,8 @@ class Page implements PageInterface return $this->template_format; } - // Use content negotitation via the `accept:` header - $http_accept = $_SERVER['HTTP_ACCEPT'] ?? false; + // Use content negotiation via the `accept:` header + $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null; if (is_string($http_accept)) { $negotiator = new Negotiator(); diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php index d83c1521d..358727f33 100644 --- a/system/src/Grav/Common/Plugins.php +++ b/system/src/Grav/Common/Plugins.php @@ -133,12 +133,25 @@ class Plugins extends Iterator */ public static function all() { - $plugins = Grav::instance()['plugins']; + $grav = Grav::instance(); + $plugins = $grav['plugins']; $list = []; foreach ($plugins as $instance) { $name = $instance->name; - $result = self::get($name); + + try { + $result = self::get($name); + } catch (\Exception $e) { + $exception = new \RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage("Plugin {$name} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } if ($result) { $list[$name] = $result; @@ -185,24 +198,31 @@ class Plugins extends Iterator $grav = Grav::instance(); $locator = $grav['locator']; - $filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); - if (!is_file($filePath)) { + $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); + + if (is_file($file)) { + // Local variables available in the file: $grav, $config, $name, $file + $class = include_once $file; + + $pluginClassFormat = [ + 'Grav\\Plugin\\' . ucfirst($name). 'Plugin', + 'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin' + ]; + + foreach ($pluginClassFormat as $pluginClass) { + if (class_exists($pluginClass)) { + $class = new $pluginClass($name, $grav); + break; + } + } + } else { $grav['log']->addWarning( sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name) ); return null; } - require_once $filePath; - - $pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin'; - if (!class_exists($pluginClassName)) { - $pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin'; - if (!class_exists($pluginClassName)) { - throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name)); - } - } - return new $pluginClassName($name, $grav); + return $class; } } diff --git a/system/src/Grav/Common/Service/SessionServiceProvider.php b/system/src/Grav/Common/Service/SessionServiceProvider.php index 856ec3596..84d23d354 100644 --- a/system/src/Grav/Common/Service/SessionServiceProvider.php +++ b/system/src/Grav/Common/Service/SessionServiceProvider.php @@ -50,13 +50,17 @@ class SessionServiceProvider implements ServiceProviderInterface // Activate admin if we're inside the admin path. $is_admin = false; if ($config->get('plugins.admin.enabled')) { - $base = '/' . trim($config->get('plugins.admin.route'), '/'); + $admin_base = '/' . trim($config->get('plugins.admin.route'), '/'); // Uri::route() is not processed yet, let's quickly get what we need. $current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH)); + // Test to see if path starts with a supported language + admin base + $lang = Utils::pathPrefixedByLangCode($current_route); + $lang_admin_base = '/' . $lang . $admin_base; + // Check no language, simple language prefix (en) and region specific language prefix (en-US). - if (Utils::startsWith($current_route, $base) || Utils::pathPrefixedByLangCode($current_route)) { + if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) { $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); $enabled = $is_admin = true; } diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index 8c0e08b6e..6720d462d 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -100,7 +100,19 @@ class Themes extends Iterator } $theme = $directory->getFilename(); - $result = $this->get($theme); + + try { + $result = $this->get($theme); + } catch (\Exception $e) { + $exception = new \RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage("Theme {$theme} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } if ($result) { $list[$theme] = $result; @@ -196,8 +208,7 @@ class Themes extends Iterator foreach ($themeClassFormat as $themeClass) { if (class_exists($themeClass)) { - $themeClassName = $themeClass; - $class = new $themeClassName($grav, $config, $name); + $class = new $themeClass($grav, $config, $name); break; } } diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index baced9abb..7e596d98a 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -1154,7 +1154,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $exif_reader = $this->grav['exif']->getReader(); - if ($image & file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { + if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { $exif_data = $exif_reader->read($image); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 811f2f433..a19373585 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -13,6 +13,7 @@ use Grav\Common\Helpers\Truncator; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\ParsedownExtra; +use Grav\Common\Page\Markdown\Excerpts; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; @@ -29,19 +30,24 @@ abstract class Utils * * @param string $input * @param bool $domain + * @param bool $fail_gracefully * @return bool|null|string */ - public static function url($input, $domain = false) + public static function url($input, $domain = false, $fail_gracefully = false) { - if (!trim((string)$input)) { - $input = '/'; + if ((!is_string($input) && !method_exists($input, '__toString')) || !trim($input)) { + if ($fail_gracefully) { + $input = '/'; + } else { + return false; + } } if (Grav::instance()['config']->get('system.absolute_urls', false)) { $domain = true; } - if (Grav::instance()['uri']->isExternal($input)) { + if (Uri::isExternal($input)) { return $input; } @@ -56,13 +62,20 @@ abstract class Utils if (Utils::contains((string)$input, '://')) { /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; - $parts = Uri::parseUrl($input); if ($parts) { - $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); + try { + $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); + } catch (\Exception $e) { + if ($fail_gracefully) { + return $input; + } else { + return false; + } + } - if (isset($parts['query'])) { + if ($resource && isset($parts['query'])) { $resource = $resource . '?' . $parts['query']; } } else { @@ -70,12 +83,13 @@ abstract class Utils $resource = $locator->findResource($input, false); } - } else { $resource = $input; } - + if (!$fail_gracefully && $resource === false) { + return false; + } return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? ''); } @@ -974,20 +988,16 @@ abstract class Utils * * @param string $string The path * - * @return bool + * @return bool|string Either false or the language * */ public static function pathPrefixedByLangCode($string) { - if (strlen($string) <= 3) { - return false; - } - $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []); $parts = explode('/', trim($string, '/')); if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) { - return true; + return $parts[0]; } return false; @@ -1396,7 +1406,7 @@ abstract class Utils $pow = min($pow, count($units) - 1); // Uncomment one of the following alternatives - $bytes /= pow(1024, $pow); + $bytes /= 1024 ** $pow; // $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . ' ' . $units[$pow]; @@ -1460,14 +1470,21 @@ abstract class Utils */ public static function processMarkdown($string, $block = true) { - $page = Grav::instance()['page'] ?? null; - $defaults = Grav::instance()['config']->get('system.pages.markdown'); + $grav = Grav::instance(); + $page = $grav['page'] ?? null; + $defaults = [ + 'markdown' => $grav['config']->get('system.pages.markdown', []), + 'images' => $grav['config']->get('system.images', []) + ]; + $extra = $defaults['markdown']['extra'] ?? false; + + $excerpts = new Excerpts($page, $defaults); // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($page, $defaults); + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); } else { - $parsedown = new Parsedown($page, $defaults); + $parsedown = new Parsedown($excerpts); } if ($block) { @@ -1486,12 +1503,11 @@ abstract class Utils * @param int $prefix * * @return string - * @throws \InvalidArgumentException if provided an invalid IP */ public static function getSubnet($ip, $prefix = 64) { if (!filter_var($ip, FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException('Invalid IP: ' . $ip); + return $ip; } // Packed representation of IP diff --git a/system/src/Grav/Console/Cli/YamlLinterCommand.php b/system/src/Grav/Console/Cli/YamlLinterCommand.php index 18cdb493f..894f1c02d 100644 --- a/system/src/Grav/Console/Cli/YamlLinterCommand.php +++ b/system/src/Grav/Console/Cli/YamlLinterCommand.php @@ -61,6 +61,15 @@ class YamlLinterCommand extends ConsoleCommand $this->displayErrors($errors, $io); } + $io->section('Page Blueprints'); + $errors = YamlLinter::lintBlueprints(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with blueprints'); + } else { + $this->displayErrors($errors, $io); + } + } protected function displayErrors($errors, $io) diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php index 840ee5b2e..8d544d4c7 100644 --- a/system/src/Grav/Console/Gpm/InstallCommand.php +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -372,7 +372,7 @@ class InstallCommand extends ConsoleCommand $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false); - $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); + $answer = $helper->ask($this->input, $this->output, $question); if (!$answer) { $this->output->writeln(" '- Skipped! "); diff --git a/system/src/Grav/Framework/Flex/FlexCollection.php b/system/src/Grav/Framework/Flex/FlexCollection.php index 5f2cbd844..5e5431bbf 100644 --- a/system/src/Grav/Framework/Flex/FlexCollection.php +++ b/system/src/Grav/Framework/Flex/FlexCollection.php @@ -9,6 +9,7 @@ namespace Grav\Framework\Flex; +use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Grav\Common\Debugger; use Grav\Common\Grav; @@ -125,7 +126,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface /** * @param array $filters - * @return FlexCollectionInterface + * @return FlexCollectionInterface|Collection */ public function filterBy(array $filters) { diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php index 7b50a7eed..c75a90152 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectory.php +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -315,15 +315,16 @@ class FlexDirectory implements FlexAuthorizeInterface $gravCache = $grav['cache']; $config = $this->getConfig('cache.' . $namespace); if (empty($config['enabled'])) { - throw new \RuntimeException(sprintf('Flex: %s %s cache not enabled', $this->type, $namespace)); - } - $timeout = $config['timeout'] ?? 60; + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); + } else { + $timeout = $config['timeout'] ?? 60; - $key = $gravCache->getKey(); - if (Utils::isAdminPlugin()) { - $key = substr($key, 0, -1); + $key = $gravCache->getKey(); + if (Utils::isAdminPlugin()) { + $key = substr($key, 0, -1); + } + $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout); } - $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout); } catch (\Exception $e) { /** @var Debugger $debugger */ $debugger = Grav::instance()['debugger']; @@ -519,7 +520,9 @@ class FlexDirectory implements FlexAuthorizeInterface // Store updated rows to the cache. if ($updated) { try { - $debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug'); + if (!$cache instanceof MemoryCache) { + $debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug'); + } $cache->setMultiple($updated); } catch (InvalidArgumentException $e) { $debugger->addException($e); @@ -640,7 +643,10 @@ class FlexDirectory implements FlexAuthorizeInterface /** @var string|FlexIndexInterface $className */ $className = $this->getIndexClass(); $keys = $className::loadEntriesFromStorage($storage); - $debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)), 'debug'); + if (!$cache instanceof MemoryCache) { + $debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)), + 'debug'); + } try { $cache->set('__keys', $keys); } catch (InvalidArgumentException $e) { diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index 7faad5227..6ded34590 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -20,6 +20,7 @@ use Grav\Framework\Form\Traits\FormTrait; use Grav\Framework\Route\Route; use Twig\Error\LoaderError; use Twig\Error\SyntaxError; +use Twig\Template; use Twig\TemplateWrapper; /** @@ -82,7 +83,7 @@ class FlexForm implements FlexFormInterface } /** - * @return Data|FlexObjectInterface + * @return Data|FlexObjectInterface|object */ public function getData() { @@ -103,7 +104,7 @@ class FlexForm implements FlexFormInterface $value = $this->data ? $this->data[$name] : null; // Return the form data or fall back to the object property. - return $value ?? $this->getObject()->value($name); + return $value ?? $this->getObject()->getFormValue($name); } public function getDefaultValue(string $name) @@ -277,7 +278,7 @@ class FlexForm implements FlexFormInterface /** * @param string $layout - * @return TemplateWrapper + * @return Template|TemplateWrapper * @throws LoaderError * @throws SyntaxError */ diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php index 29ac33bbc..8b96b4a9a 100644 --- a/system/src/Grav/Framework/Flex/FlexIndex.php +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -312,9 +312,10 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde // Ordering can be done by using index only. $previous = null; foreach (array_reverse($orderings) as $field => $ordering) { + $field = (string)$field; if ($this->getKeyField() === $field) { $keys = $this->getKeys(); - $search = array_combine($keys, $keys); + $search = array_combine($keys, $keys) ?: []; } elseif ($field === 'flex_key') { $search = $this->getFlexKeys(); } else { @@ -462,7 +463,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde $first = reset($entries); if ($first) { $keys = array_keys($first); - $keys = array_combine($keys, $keys); + $keys = array_combine($keys, $keys) ?: []; } else { $keys = []; } diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index 51f622a7a..01f45d079 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -31,6 +31,7 @@ use Psr\SimpleCache\InvalidArgumentException; use RocketTheme\Toolbox\Event\Event; use Twig\Error\LoaderError; use Twig\Error\SyntaxError; +use Twig\Template; use Twig\TemplateWrapper; /** @@ -391,7 +392,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface } try { - $data = $cache ? $cache->get($key) : null; + $data = $cache && $key ? $cache->get($key) : null; $block = $data ? HtmlBlock::fromArray($data) : null; } catch (InvalidArgumentException $e) { @@ -410,7 +411,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface } if (!$block) { - $block = HtmlBlock::create($key); + $block = HtmlBlock::create($key ?: null); $block->setChecksum($checksum); if ($key === false) { $block->disableCache(); @@ -434,7 +435,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface $block->setContent($output); try { - $cache && $block->isCached() && $cache->set($key, $block->toArray()); + $cache && $key && $block->isCached() && $cache->set($key, $block->toArray()); } catch (InvalidArgumentException $e) { $debugger->addException($e); } @@ -541,7 +542,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface $result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]); $value = reset($result); - $storageKey = key($result); + $storageKey = (string)key($result); if ($value && $storageKey) { $this->setStorageKey($storageKey); if (!$this->hasKey()) { @@ -558,7 +559,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface if (method_exists($this, 'clearMediaCache')) { $this->clearMediaCache(); } - } catch (InvalidArgumentException $e) { + } catch (\Exception $e) { /** @var Debugger $debugger */ $debugger = Grav::instance()['debugger']; $debugger->addException($e); @@ -586,7 +587,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface if (method_exists($this, 'clearMediaCache')) { $this->clearMediaCache(); } - } catch (InvalidArgumentException $e) { + } catch (\Exception $e) { /** @var Debugger $debugger */ $debugger = Grav::instance()['debugger']; $debugger->addException($e); @@ -736,7 +737,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface } $grav = Grav::instance(); - /** @var Flex $flex */ + /** @var Flex|null $flex */ $flex = $grav['flex_objects'] ?? null; $directory = $flex ? $flex->getDirectory($type) : null; if (!$directory) { @@ -807,7 +808,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** * @param string $layout - * @return TemplateWrapper + * @return Template|TemplateWrapper * @throws LoaderError * @throws SyntaxError */ diff --git a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php index b547cc227..cc460ccd0 100644 --- a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php @@ -128,7 +128,7 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface return $path; } - return (string) $locator->findResource($path) ?: $locator->findResource($path, true, true); + return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true)); } /** diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php index 2304e8013..76397c235 100644 --- a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php @@ -455,6 +455,8 @@ class FolderStorage extends AbstractFilesystemStorage protected function initOptions(array $options): void { $extension = $this->dataFormatter->getDefaultFileExtension(); + + /** @var string $pattern */ $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern; $this->dataFolder = $options['folder']; diff --git a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php index 209ba3382..088b9527e 100644 --- a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php @@ -227,7 +227,12 @@ class SimpleStorage extends AbstractFilesystemStorage $keys = array_keys($this->data); $keys[array_search($src, $keys, true)] = $dst; - $this->data = array_combine($keys, $this->data); + $data = array_combine($keys, $this->data); + if (false === $data) { + throw new \LogicException('Bad data'); + } + + $this->data = $data; return true; } diff --git a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php index 0029d9fe0..643004e74 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php @@ -26,6 +26,7 @@ trait FlexAuthorizeTrait public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool { if (null === $user) { + /** @var UserInterface $user */ $user = Grav::instance()['user']; } diff --git a/system/src/Grav/Framework/Interfaces/RenderInterface.php b/system/src/Grav/Framework/Interfaces/RenderInterface.php index 9185c1712..ca6d9bca4 100644 --- a/system/src/Grav/Framework/Interfaces/RenderInterface.php +++ b/system/src/Grav/Framework/Interfaces/RenderInterface.php @@ -29,7 +29,7 @@ interface RenderInterface * @example {% render object layout 'custom' with { variable: 'value' } %} * * @param string|null $layout Layout to be used. - * @param array|null $context Extra context given to the renderer. + * @param array $context Extra context given to the renderer. * * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. * @api diff --git a/system/src/Grav/Framework/Uri/UriPartsFilter.php b/system/src/Grav/Framework/Uri/UriPartsFilter.php index 88fe3e64e..51a971ab4 100644 --- a/system/src/Grav/Framework/Uri/UriPartsFilter.php +++ b/system/src/Grav/Framework/Uri/UriPartsFilter.php @@ -84,11 +84,11 @@ class UriPartsFilter */ public static function filterPort($port = null) { - if (null === $port || (\is_int($port) && ($port >= 1 && $port <= 65535))) { + if (null === $port || (\is_int($port) && ($port >= 0 && $port <= 65535))) { return $port; } - throw new \InvalidArgumentException('Uri port must be null or an integer between 1 and 65535'); + throw new \InvalidArgumentException('Uri port must be null or an integer between 0 and 65535'); } /** diff --git a/tests/phpstan/phpstan.neon b/tests/phpstan/phpstan.neon index e8e24ff51..29781281b 100644 --- a/tests/phpstan/phpstan.neon +++ b/tests/phpstan/phpstan.neon @@ -41,14 +41,15 @@ parameters: message: '#Grav\\Common\\GPM\\Remote\\GravCore::__construct\(\) does not call parent constructor from Grav\\Common\\GPM\\Remote\\AbstractPackageCollection#' path: 'system/src/Grav/Common/GPM/Remote/GravCore.php' + # PSR-16 Exception interfaces do not extend \Throwable + - '#PHPDoc tag \@throws with type Psr\\SimpleCache\\(CacheException|InvalidArgumentException) is not subtype of Throwable#' + - '#expects Exception, Psr\\SimpleCache\\InvalidArgumentException&Throwable given#' + # Needed: psr-17 (http-factories) support (through decorator or further investigations) - message: '#Call to an undefined static method Grav\\Framework\\Psr7\\Stream::create\(\)#' path: 'system/src/Grav/Framework/Form/FormFlashFile.php' - # PSR-16 Exception interfaces do not extend \Throwable - - '#PHPDoc tag \@throws with type Psr\\SimpleCache\\(CacheException|InvalidArgumentException) is not subtype of Throwable#' - # Medium __call() methods - '#Call to an undefined method Grav\\Common\\Page\\Medium\\(\w*)Medium::#' diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php index e73e11f01..58a1acf61 100644 --- a/tests/unit/Grav/Common/Markdown/ParsedownTest.php +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -2,6 +2,7 @@ use Codeception\Util\Fixtures; use Grav\Common\Grav; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Uri; use Grav\Common\Config\Config; use Grav\Common\Page\Pages; @@ -56,14 +57,19 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->pages->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/item2/item2-2'); - $this->parsedown = new Parsedown($page, $defaults); + + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); } protected function _after() @@ -179,14 +185,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

', $this->parsedown->text('![](home-sample-image.jpg)')); @@ -230,15 +240,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); - + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

Down a Level

', $this->parsedown->text('[Down a Level](item1-3)')); diff --git a/tests/unit/Grav/Common/UtilsTest.php b/tests/unit/Grav/Common/UtilsTest.php index f3b2a9707..e5be6394b 100644 --- a/tests/unit/Grav/Common/UtilsTest.php +++ b/tests/unit/Grav/Common/UtilsTest.php @@ -379,14 +379,25 @@ class UtilsTest extends \Codeception\TestCase\Test { $this->uri->initializeWithUrl('http://testing.dev/path1/path2')->init(); - $this->assertSame('http://testing.dev/', Utils::url('/', true)); - $this->assertSame('http://testing.dev/', Utils::url('', true)); - $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url('foo://bar/baz')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + + // Fail Gracefully + $this->assertSame('/', Utils::url('/', false, true)); + $this->assertSame('/', Utils::url('', false, true)); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', false, true)); + $this->assertSame('/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/', Utils::url('/')); - $this->assertSame('/', Utils::url('')); + $this->assertSame('http://testing.dev/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); $this->assertSame('/path1', Utils::url('/path1')); $this->assertSame('/path1/path2', Utils::url('/path1/path2')); - $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); @@ -394,18 +405,27 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertSame('/foobar.jpg', Utils::url('foobar.jpg')); $this->assertSame('/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); $this->assertSame('/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + } public function testUrlWithRoot() { $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/path1/path2', '/subdir')->init(); + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url('foo://bar/baz')); + + // Fail Gracefully + $this->assertSame('/subdir/', Utils::url('/', false, true)); + $this->assertSame('/subdir/', Utils::url('', false, true)); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', false, true)); + $this->assertSame('http://testing.dev/subdir/', Utils::url('/', true)); - $this->assertSame('http://testing.dev/subdir/', Utils::url('', true)); $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/path1', true)); $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/subdir/path1', true)); $this->assertSame('/subdir/', Utils::url('/')); - $this->assertSame('/subdir/', Utils::url('')); $this->assertSame('/subdir/path1', Utils::url('/path1')); $this->assertSame('/subdir/path1/path2', Utils::url('/path1/path2')); $this->assertSame('/subdir/path1/path2', Utils::url('/subdir/path1/path2'));