diff --git a/CHANGELOG.md b/CHANGELOG.md index c394dadf0..28c2db455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.7.31 +## mm/dd/2022 + +1. [](#new) + * Added support to get image size for SVG vector images [#3533](https://github.com/getgrav/grav/pull/3533) + * Added XSS check for uploaded SVG files before they get stored + * Fixed phpstan issues (All level 2, Framework level 5) +2. [](#bugfix) + * Fixed `'mbstring' extension is not loaded` error, use Polyfill instead [#3504](https://github.com/getgrav/grav/pull/3504) + * Fixed new `Utils::pathinfo()` and `Utils::basename()` being too strict for legacy use [#3542](https://github.com/getgrav/grav/issues/3542) + * Fixed non-standard video html atributes generated by `{{ media.html() }}` [#3540](https://github.com/getgrav/grav/issues/3540) + * Fixed entity sanitization for XSS detection + * Fixed avatar save location when `account://` stream points to custom directory + # v1.7.30 ## 02/07/2022 diff --git a/bin/gpm b/bin/gpm index 9e58706f6..12d0c3d36 100755 --- a/bin/gpm +++ b/bin/gpm @@ -25,14 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){ $autoload = require __DIR__ . '/../vendor/autoload.php'; -if (!ini_get('date.timezone')) { - date_default_timezone_set('UTC'); -} +// Set timezone to default, falls back to system if php.ini not set +date_default_timezone_set(@date_default_timezone_get()); // Set internal encoding. -if (!\extension_loaded('mbstring')) { - die("'mbstring' extension is not loaded. This is required for Grav to run correctly"); -} @ini_set('default_charset', 'UTF-8'); mb_internal_encoding('UTF-8'); diff --git a/bin/grav b/bin/grav index cc925c394..1bcba715a 100755 --- a/bin/grav +++ b/bin/grav @@ -25,14 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){ $autoload = require __DIR__ . '/../vendor/autoload.php'; -if (!ini_get('date.timezone')) { - date_default_timezone_set('UTC'); -} +// Set timezone to default, falls back to system if php.ini not set +date_default_timezone_set(@date_default_timezone_get()); // Set internal encoding. -if (!\extension_loaded('mbstring')) { - die("'mbstring' extension is not loaded. This is required for Grav to run correctly"); -} @ini_set('default_charset', 'UTF-8'); mb_internal_encoding('UTF-8'); diff --git a/bin/plugin b/bin/plugin index 3a784af57..4a3683858 100755 --- a/bin/plugin +++ b/bin/plugin @@ -25,14 +25,10 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')){ $autoload = require __DIR__ . '/../vendor/autoload.php'; -if (!ini_get('date.timezone')) { - date_default_timezone_set('UTC'); -} +// Set timezone to default, falls back to system if php.ini not set +date_default_timezone_set(@date_default_timezone_get()); // Set internal encoding. -if (!\extension_loaded('mbstring')) { - die("'mbstring' extension is not loaded. This is required for Grav to run correctly"); -} @ini_set('default_charset', 'UTF-8'); mb_internal_encoding('UTF-8'); diff --git a/composer.json b/composer.json index 511690104..db5449738 100644 --- a/composer.json +++ b/composer.json @@ -115,8 +115,8 @@ "scripts": { "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md", "post-create-project-cmd": "bin/grav install", - "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=720M system/src", - "phpstan-framework": "vendor/bin/phpstan analyse -l 4 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer", + "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon --memory-limit=720M system/src", + "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer", "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins", "test": "vendor/bin/codecept run unit", "test-windows": "vendor\\bin\\codecept run unit" diff --git a/composer.lock b/composer.lock index 1f73769f4..965a63c1f 100644 --- a/composer.lock +++ b/composer.lock @@ -840,22 +840,21 @@ }, { "name": "itsgoingd/clockwork", - "version": "v5.1.4", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "7252aa771b77ac8678b44290fd7ec7577435cce6" + "reference": "6a7b3942224fa53cf3704d9adba636e1f3dfeb9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/7252aa771b77ac8678b44290fd7ec7577435cce6", - "reference": "7252aa771b77ac8678b44290fd7ec7577435cce6", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/6a7b3942224fa53cf3704d9adba636e1f3dfeb9a", + "reference": "6a7b3942224fa53cf3704d9adba636e1f3dfeb9a", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=5.6", - "psr/log": "1.* || ^2.0" + "php": ">=5.6" }, "type": "library", "extra": { @@ -897,7 +896,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.4" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.5" }, "funding": [ { @@ -905,7 +904,7 @@ "type": "github" } ], - "time": "2022-01-30T12:36:18+00:00" + "time": "2022-02-13T22:57:42+00:00" }, { "name": "league/climate", @@ -1105,25 +1104,26 @@ }, { "name": "maximebf/debugbar", - "version": "v1.17.3", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "e8ac3499af0ea5b440908e06cc0abe5898008b3c" + "reference": "0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/e8ac3499af0ea5b440908e06cc0abe5898008b3c", - "reference": "e8ac3499af0ea5b440908e06cc0abe5898008b3c", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6", + "reference": "0d44b75f3b5d6d41ae83b79c7a4bceae7fbc78b6", "shasum": "" }, "require": { "php": "^7.1|^8", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^2.6|^3|^4|^5" + "symfony/var-dumper": "^2.6|^3|^4|^5|^6" }, "require-dev": { - "phpunit/phpunit": "^7.5.20 || ^9.4.2" + "phpunit/phpunit": "^7.5.20 || ^9.4.2", + "twig/twig": "^1.38|^2.7|^3.0" }, "suggest": { "kriswallsmith/assetic": "The best way to manage assets", @@ -1164,9 +1164,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.17.3" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.18.0" }, - "time": "2021-10-19T12:33:27+00:00" + "time": "2021-12-27T18:49:48+00:00" }, { "name": "miljar/php-exif", @@ -2730,12 +2730,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4019,12 +4019,12 @@ } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4356,16 +4356,16 @@ }, { "name": "phar-io/version", - "version": "3.1.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "15a90844ad40f127afd244c0cad228de2a80052a" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/15a90844ad40f127afd244c0cad228de2a80052a", - "reference": "15a90844ad40f127afd244c0cad228de2a80052a", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { @@ -4401,9 +4401,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.1" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-07T21:56:48+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4748,16 +4748,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.10", + "version": "9.2.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687" + "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/665a1ac0a763c51afc30d6d130dac0813092b17f", + "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f", "shasum": "" }, "require": { @@ -4813,7 +4813,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.11" }, "funding": [ { @@ -4821,7 +4821,7 @@ "type": "github" } ], - "time": "2021-12-05T09:12:13+00:00" + "time": "2022-02-18T12:46:09+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5066,16 +5066,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.13", + "version": "9.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743" + "reference": "1883687169c017d6ae37c58883ca3994cfc34189" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/597cb647654ede35e43b137926dfdfef0fb11743", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1883687169c017d6ae37c58883ca3994cfc34189", + "reference": "1883687169c017d6ae37c58883ca3994cfc34189", "shasum": "" }, "require": { @@ -5153,7 +5153,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.14" }, "funding": [ { @@ -5165,7 +5165,7 @@ "type": "github" } ], - "time": "2022-01-24T07:33:35+00:00" + "time": "2022-02-18T12:54:07+00:00" }, { "name": "psr/http-client", @@ -5725,16 +5725,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.3", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { @@ -5777,7 +5777,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" }, "funding": [ { @@ -5785,7 +5785,7 @@ "type": "github" } ], - "time": "2021-06-11T13:31:12+00:00" + "time": "2022-02-14T08:28:10+00:00" }, { "name": "sebastian/lines-of-code", diff --git a/index.php b/index.php index 3e308312f..b6b86e1c0 100644 --- a/index.php +++ b/index.php @@ -20,16 +20,6 @@ if (PHP_SAPI === 'cli-server') { } } -// Set timezone to default, falls back to system if php.ini not set -date_default_timezone_set(@date_default_timezone_get()); - -// Set internal encoding. -if (!\extension_loaded('mbstring')) { - die("'mbstring' extension is not loaded. This is required for Grav to run correctly"); -} -@ini_set('default_charset', 'UTF-8'); -mb_internal_encoding('UTF-8'); - // Ensure vendor libraries exist $autoload = __DIR__ . '/vendor/autoload.php'; if (!is_file($autoload)) { @@ -39,23 +29,23 @@ if (!is_file($autoload)) { // Register the auto-loader. $loader = require $autoload; +// Set timezone to default, falls back to system if php.ini not set +date_default_timezone_set(@date_default_timezone_get()); + +// Set internal encoding. +@ini_set('default_charset', 'UTF-8'); +mb_internal_encoding('UTF-8'); + use Grav\Common\Grav; use RocketTheme\Toolbox\Event\Event; // Get the Grav instance -$grav = Grav::instance( - array( - 'loader' => $loader - ) -); +$grav = Grav::instance(array('loader' => $loader)); // Process the page try { $grav->process(); -} catch (\Error $e) { - $grav->fireEvent('onFatalException', new Event(array('exception' => $e))); - throw $e; -} catch (\Exception $e) { +} catch (\Error|\Exception $e) { $grav->fireEvent('onFatalException', new Event(array('exception' => $e))); throw $e; } diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index cfe537620..391c7370a 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -11,7 +11,7 @@ form: avatar: type: file size: large - destination: 'user://accounts/avatars' + destination: 'account://avatars' multiple: false random_name: true diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php index 134c62fba..621a14041 100644 --- a/system/src/Grav/Common/Assets.php +++ b/system/src/Grav/Common/Assets.php @@ -16,8 +16,8 @@ use Grav\Common\Assets\Traits\TestingAssetsTrait; use Grav\Common\Config\Config; use Grav\Framework\Object\PropertyObject; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; +use function array_slice; use function call_user_func_array; -use function count; use function func_get_args; use function is_array; @@ -174,6 +174,10 @@ class Assets extends PropertyObject */ public function add($asset) { + if (!$asset) { + return $this; + } + $args = func_get_args(); // More than one asset @@ -198,7 +202,8 @@ class Assets extends PropertyObject call_user_func_array([$this, 'add'], $args); } else { // Get extension - $extension = Utils::pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION); + $path = parse_url($asset, PHP_URL_PATH); + $extension = $path ? Utils::pathinfo($path, PATHINFO_EXTENSION) : ''; // JavaScript or CSS if ($extension !== '') { @@ -525,8 +530,8 @@ class Assets extends PropertyObject /** * Build the Javascript Modules tags * - * @param $group - * @param $attributes + * @param string $group + * @param array $attributes * @return string */ public function jsModule($group = 'head', $attributes = []) @@ -534,6 +539,11 @@ class Assets extends PropertyObject return $this->render(self::JS_MODULE, $group, $attributes); } + /** + * @param string $group + * @param array $attributes + * @return string + */ public function all($group = 'head', $attributes = []) { $output = $this->css($group, $attributes, false); @@ -543,11 +553,19 @@ class Assets extends PropertyObject return $output; } + /** + * @param class-string $type + * @return bool + */ protected function isValidType($type) { return in_array($type, [self::CSS_TYPE, self::JS_TYPE, self::JS_MODULE_TYPE]); } + /** + * @param class-string $type + * @return string + */ protected function getBaseType($type) { switch ($type) { diff --git a/system/src/Grav/Common/Assets/BlockAssets.php b/system/src/Grav/Common/Assets/BlockAssets.php index 798bdd304..7d9796090 100644 --- a/system/src/Grav/Common/Assets/BlockAssets.php +++ b/system/src/Grav/Common/Assets/BlockAssets.php @@ -55,7 +55,7 @@ class BlockAssets /** * @param Assets $assets - * @param array $groups + * @param array $list * @return void */ protected static function registerFrameworks(Assets $assets, array $list): void diff --git a/system/src/Grav/Common/Backup/Backups.php b/system/src/Grav/Common/Backup/Backups.php index 9c23908c4..ff4789d86 100644 --- a/system/src/Grav/Common/Backup/Backups.php +++ b/system/src/Grav/Common/Backup/Backups.php @@ -104,7 +104,7 @@ class Backups */ public function getBackupDownloadUrl($backup, $base_url) { - $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':'); + $param_sep = Grav::instance()['config']->get('system.param_sep', ':'); $download = urlencode(base64_encode(Utils::basename($backup))); $url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim( $base_url, diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index c0ca5c405..f3978bc1d 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -606,8 +606,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface } } + /** @var PageCollection|PageIndex $children */ + $children = $page->children(); /** @var PageIndex $children */ - $children = $page->children()->getIndex(); + $children = $children->getIndex(); $selectedChildren = $children->filterBy($filters, true); /** @var Header $header */ @@ -686,6 +688,8 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $extras = array_filter($extras, static function ($v) { return $v !== null; }); + + /** @var PageIndex $tmp */ $tmp = $child->children()->getIndex(); $child_count = $tmp->count(); $count = $filters ? $tmp->filterBy($filters, true)->count() : null; diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php index fed11b437..a05f0eb42 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php @@ -625,7 +625,14 @@ class PageObject extends FlexPageObject // If current filter does not match, we still may have match as a parent. if ($matches === false) { - return $recursive && $this->children()->getIndex()->filterBy($filters, true)->count() > 0; + if (!$recursive) { + return false; + } + + /** @var PageIndex $index */ + $index = $this->children()->getIndex(); + + return $index->filterBy($filters, true)->count() > 0; } } diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php index d9217917d..9dec9729f 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserObject.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -666,7 +666,7 @@ class UserObject extends FlexObject implements UserInterface, Countable // Check for shared media if (!$folder && !$this->getFlexDirectory()->getMediaFolder()) { $this->_loadMedia = false; - $folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'user://accounts/avatars'; + $folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'account://avatars'; } return $folder; diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 18d490295..b9b44a4df 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -62,6 +62,7 @@ use function call_user_func_array; use function function_exists; use function get_class; use function in_array; +use function is_array; use function is_callable; use function is_int; use function is_string; @@ -729,14 +730,17 @@ class Grav extends Container */ public function fallbackUrl($path) { + $path_parts = Utils::pathinfo($path); + if (!is_array($path_parts)) { + return false; + } + /** @var Uri $uri */ $uri = $this['uri']; /** @var Config $config */ $config = $this['config']; - $path_parts = Utils::pathinfo($path); - /** @var Pages $pages */ $pages = $this['pages']; $page = $pages->find($path_parts['dirname'], true); diff --git a/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php index 4af3052b0..933475962 100644 --- a/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php +++ b/system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php @@ -16,6 +16,8 @@ use Grav\Common\Data\Data; * Class implements media object interface. * * @property string $type + * @property string $filename + * @property string $filepath */ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObjectInterface, ArrayAccess { diff --git a/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php index 06cb99ad9..7e59d64b0 100644 --- a/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php +++ b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php @@ -25,7 +25,7 @@ trait MediaPlayerTrait public function controls($status = true) { if ($status) { - $this->attributes['controls'] = true; + $this->attributes['controls'] = 'controls'; } else { unset($this->attributes['controls']); } @@ -42,7 +42,7 @@ trait MediaPlayerTrait public function loop($status = false) { if ($status) { - $this->attributes['loop'] = true; + $this->attributes['loop'] = 'loop'; } else { unset($this->attributes['loop']); } @@ -59,7 +59,7 @@ trait MediaPlayerTrait public function autoplay($status = false) { if ($status) { - $this->attributes['autoplay'] = true; + $this->attributes['autoplay'] = 'autoplay'; } else { unset($this->attributes['autoplay']); } @@ -76,7 +76,7 @@ trait MediaPlayerTrait public function muted($status = false) { if ($status) { - $this->attributes['muted'] = true; + $this->attributes['muted'] = 'muted'; } else { unset($this->attributes['muted']); } @@ -108,6 +108,6 @@ trait MediaPlayerTrait */ public function resetPlayer() { - $this->attributes['controls'] = true; + $this->attributes['controls'] = 'controls'; } } diff --git a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php index 36a4503f1..88591f6ae 100644 --- a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php +++ b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php @@ -100,6 +100,10 @@ trait MediaUploadTrait 'size' => $uploadedFile->getSize(), ]; + if ($uploadedFile instanceof FormFlashFile) { + $uploadedFile->checkXss(); + } + return $this->checkFileMetadata($metadata, $filename, $settings); } diff --git a/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php index b16bb53c7..07f0c3f12 100644 --- a/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php +++ b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php @@ -40,7 +40,7 @@ trait VideoMediaTrait public function playsinline($status = false) { if ($status) { - $this->attributes['playsinline'] = true; + $this->attributes['playsinline'] = 'playsinline'; } else { unset($this->attributes['playsinline']); } diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php index 620446b70..913f198f1 100644 --- a/system/src/Grav/Common/Page/Medium/MediumFactory.php +++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php @@ -159,8 +159,9 @@ class MediumFactory return new ImageMedium($items, $blueprint); case 'thumbnail': return new ThumbnailImageMedium($items, $blueprint); - case 'animated': case 'vector': + return new VectorImageMedium($items, $blueprint); + case 'animated': return new StaticImageMedium($items, $blueprint); case 'video': return new VideoMedium($items, $blueprint); diff --git a/system/src/Grav/Common/Page/Medium/VectorImageMedium.php b/system/src/Grav/Common/Page/Medium/VectorImageMedium.php new file mode 100644 index 000000000..c44f35a77 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/VectorImageMedium.php @@ -0,0 +1,68 @@ +get('width'); + $height = $this->get('height'); + if ($width && $height) { + return; + } + + // Make sure that getting image size is supported. + if ($this->mime !== 'image/svg+xml' || !\extension_loaded('simplexml')) { + return; + } + + // Make sure that the image exists. + $path = $this->get('filepath'); + if (!$path || !file_exists($path) || !filesize($path)) { + return; + } + + $xml = simplexml_load_string(file_get_contents($path)); + $attr = $xml ? $xml->attributes() : null; + if (!$attr instanceof \SimpleXMLElement) { + return; + } + + // Get the size from svg image. + if ($attr->width && $attr->height) { + $width = (string)$attr->width; + $height = (string)$attr->height; + } elseif ($attr->viewBox && \count($size = explode(' ', (string)$attr->viewBox)) === 4) { + [,$width,$height,] = $size; + } + + if ($width && $height) { + $this->def('width', (int)$width); + $this->def('height', (int)$height); + } + } +} diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 8ad2ccd8f..ec48b5173 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -736,7 +736,13 @@ class Pages break; case 'siblings': $parent = $page->parent(); - $collection = $parent ? $parent->children()->remove($page->path()) : new Collection(); + if ($parent) { + /** @var Collection $collection */ + $collection = $parent->children(); + $collection = $collection->remove($page->path()); + } else { + $collection = new Collection(); + } break; case 'descendants': $collection = $this->all($page)->remove($page->path())->pages(); @@ -1041,9 +1047,14 @@ class Pages $this->grav->redirectLangSafe($page->redirect()); } - if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) { - // Redirect to the first visible child as current page isn't routable. - $this->grav->redirectLangSafe($child->route()); + if (!$routable) { + /** @var Collection $children */ + $children = $page->children()->visible()->routable()->published(); + $child = $children->first(); + if ($child !== null) { + // Redirect to the first visible child as current page isn't routable. + $this->grav->redirectLangSafe($child->route()); + } } } diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php index 017720ca8..779e61918 100644 --- a/system/src/Grav/Common/Security.php +++ b/system/src/Grav/Common/Security.php @@ -25,6 +25,22 @@ use function is_string; */ class Security { + /** + * @param string $filepath + * @param array|null $options + * @return string|null + */ + public static function detectXssFromSvgFile(string $filepath, array $options = null): ?string + { + if (file_exists($filepath) && Grav::instance()['config']->get('security.sanitize_svg')) { + $content = file_get_contents($filepath); + + return static::detectXss($content, $options); + } + + return null; + } + /** * Sanitize SVG string for XSS code * @@ -200,7 +216,7 @@ class Security }, $string); // Clean up entities - $string = preg_replace('!(�+[0-9]+)!u', '$1;', $string); + $string = preg_replace('!(&#[0-9]+);?!u', '$1;', $string); // Decode entities $string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8'); diff --git a/system/src/Grav/Common/Twig/TwigEnvironment.php b/system/src/Grav/Common/Twig/TwigEnvironment.php index 6b2a86bef..dba884693 100644 --- a/system/src/Grav/Common/Twig/TwigEnvironment.php +++ b/system/src/Grav/Common/Twig/TwigEnvironment.php @@ -11,6 +11,8 @@ namespace Grav\Common\Twig; use Twig\Environment; use Twig\Error\LoaderError; +use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\LoaderInterface; use Twig\Template; use Twig\TemplateWrapper; @@ -41,8 +43,12 @@ class TwigEnvironment extends Environment } // Optimization: Avoid throwing an exception when it would be ignored anyway. - if (1 !== $count && !$this->getLoader()->exists($name)) { - continue; + if (1 !== $count) { + /** @var LoaderInterface|ExistsLoaderInterface $loader */ + $loader = $this->getLoader(); + if (!$loader->exists($name)) { + continue; + } } // Throws LoaderError: Unable to find template "%s". diff --git a/system/src/Grav/Common/User/DataUser/User.php b/system/src/Grav/Common/User/DataUser/User.php index 71e7f9b35..9cd3904f0 100644 --- a/system/src/Grav/Common/User/DataUser/User.php +++ b/system/src/Grav/Common/User/DataUser/User.php @@ -193,7 +193,7 @@ class User extends Data implements UserInterface */ public function getMediaFolder() { - return $this->blueprints()->fields()['avatar']['destination'] ?? 'user://accounts/avatars'; + return $this->blueprints()->fields()['avatar']['destination'] ?? 'account://avatars'; } /** diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index d0e0fab15..a1ba549e8 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -994,7 +994,7 @@ abstract class Utils * @param int|null $flags * @return array|string */ - public static function pathinfo(string $path, int $flags = null) + public static function pathinfo($path, int $flags = null) { $path = str_replace(['%2F', '%5C'], ['/', '\\'], rawurlencode($path)); @@ -1020,7 +1020,7 @@ abstract class Utils * @param string $suffix * @return string */ - public static function basename(string $path, string $suffix = ''): string + public static function basename($path, string $suffix = ''): string { return rawurldecode(basename(str_replace(['%2F', '%5C'], '/', rawurlencode($path)), $suffix)); } diff --git a/system/src/Grav/Framework/Form/FormFlashFile.php b/system/src/Grav/Framework/Form/FormFlashFile.php index 6c995993e..65af544d1 100644 --- a/system/src/Grav/Framework/Form/FormFlashFile.php +++ b/system/src/Grav/Framework/Form/FormFlashFile.php @@ -9,6 +9,8 @@ namespace Grav\Framework\Form; +use Grav\Common\Security; +use Grav\Common\Utils; use Grav\Framework\Psr7\Stream; use InvalidArgumentException; use JsonSerializable; @@ -182,6 +184,21 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable return $this->upload; } + /** + * @return void + */ + public function checkXss(): void + { + $tmpFile = $this->getTmpFile(); + $mime = $this->getClientMediaType(); + if (Utils::contains($mime, 'svg', false)) { + $response = Security::detectXssFromSvgFile($tmpFile); + if ($response) { + throw new RuntimeException(sprintf('SVG file XSS check failed on %s', $response)); + } + } + } + /** * @return string|null */