This commit is contained in:
2022-03-15 14:53:40 +01:00
parent 22adba5d54
commit 87668f1c0b
33 changed files with 538 additions and 259 deletions
+22 -4
View File
@@ -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) {
@@ -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
+1 -1
View File
@@ -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,
@@ -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;
@@ -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;
}
}
@@ -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;
+6 -2
View File
@@ -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);
@@ -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
{
@@ -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';
}
}
@@ -100,6 +100,10 @@ trait MediaUploadTrait
'size' => $uploadedFile->getSize(),
];
if ($uploadedFile instanceof FormFlashFile) {
$uploadedFile->checkXss();
}
return $this->checkFileMetadata($metadata, $filename, $settings);
}
@@ -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']);
}
@@ -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);
@@ -0,0 +1,68 @@
<?php
/**
* @package Grav\Common\Page
*
* @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Data\Blueprint;
/**
* Class StaticImageMedium
* @package Grav\Common\Page\Medium
*/
class VectorImageMedium extends StaticImageMedium
{
/**
* Construct.
*
* @param array $items
* @param Blueprint|null $blueprint
*/
public function __construct($items = [], Blueprint $blueprint = null)
{
parent::__construct($items, $blueprint);
// If we already have the image size, we do not need to do anything else.
$width = $this->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);
}
}
}
+15 -4
View File
@@ -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());
}
}
}
+17 -1
View File
@@ -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+[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');
@@ -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".
@@ -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';
}
/**
@@ -9,12 +9,15 @@
namespace Grav\Common\User\Traits;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\StaticImageMedium;
use Grav\Common\User\Authentication;
use Grav\Common\Utils;
use Multiavatar;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function is_array;
use function is_string;
@@ -175,9 +178,52 @@ trait UserTrait
}
$email = $this->get('email');
$avatar_generator = Grav::instance()['config']->get('system.accounts.avatar', 'multiavatar');
if ($avatar_generator === 'gravatar') {
if (!$email) {
return '';
}
$hash = md5(strtolower(trim($email)));
return 'https://www.gravatar.com/avatar/' . $hash;
}
$hash = $this->get('avatar_hash');
if (!$hash) {
$username = $this->get('username');
$hash = md5(strtolower(trim($email ?? $username)));
}
return $this->generateMultiavatar($hash);
}
/**
* @param string $hash
* @return string
*/
protected function generateMultiavatar(string $hash): string
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$storage = $locator->findResource('image://multiavatar', true, true);
$avatar_file = "{$storage}/{$hash}.svg";
if (!file_exists($storage)) {
Folder::create($storage);
}
if (!file_exists($avatar_file)) {
$mavatar = new Multiavatar();
file_put_contents($avatar_file, $mavatar->generate($hash, null, null));
}
$avatar_url = $locator->findResource("image://multiavatar/{$hash}.svg", false, true);
return Utils::url($avatar_url);
// By default fall back to gravatar image.
return $email ? 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($email))) : '';
}
abstract public function get($name, $default = null, $separator = null);
+6 -7
View File
@@ -134,14 +134,13 @@ abstract class Utils
$resource = $locator->findResource($input, false);
}
} else {
$root = $uri->rootUrl();
if (static::startsWith($input, $root)) {
$input = static::replaceFirstOccurrence($root, '', $input);
$root = preg_quote($uri->rootUrl(), '#');
$pattern = '#(' . $root . '$|' . $root . '/)#';
if (!empty($root) && preg_match($pattern, $input, $matches)) {
$input = static::replaceFirstOccurrence($matches[0], '', $input);
}
$input = ltrim($input, '/');
$resource = $input;
}
@@ -994,7 +993,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 +1019,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));
}
@@ -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
*/
@@ -23,6 +23,7 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Session\SessionInterface;
@@ -775,13 +776,16 @@ trait FormTrait
{
// Handle bad filenames.
$filename = $file->getClientFilename();
if ($filename && !Utils::checkFilename($filename)) {
$grav = Grav::instance();
throw new RuntimeException(
sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
);
}
if ($file instanceof FormFlashFile) {
$file->checkXss();
}
}
/**