Update to Kirby 4.7.0

This commit is contained in:
Paul Nicoué 2025-04-21 18:57:21 +02:00
parent 02a9ab387c
commit ba25a9a198
509 changed files with 26604 additions and 14872 deletions

View file

@ -16,6 +16,8 @@ use Kirby\Toolkit\Dom;
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* @SuppressWarnings(PHPMD.LongVariable)
*/
class DomHandler extends Handler
{
@ -43,6 +45,13 @@ class DomHandler extends Handler
*/
public static array|bool $allowedDomains = true;
/**
* Whether URLs that begin with `/` should be allowed even if the
* site index URL is in a subfolder (useful when using the HTML
* `<base>` element where the sanitized code will be rendered)
*/
public static bool $allowHostRelativeUrls = true;
/**
* Names of allowed XML processing instructions
*/
@ -57,27 +66,34 @@ class DomHandler extends Handler
/**
* Sanitizes the given string
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
*/
public static function sanitize(string $string): string
public static function sanitize(string $string, bool $isExternal = false): string
{
$dom = static::parse($string);
$dom->sanitize(static::options());
$dom->sanitize(static::options($isExternal));
return $dom->toString();
}
/**
* Validates file contents
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
*/
public static function validate(string $string): void
public static function validate(string $string, bool $isExternal = false): void
{
$dom = static::parse($string);
$errors = $dom->sanitize(static::options());
$dom = static::parse($string);
$errors = $dom->sanitize(static::options($isExternal));
// there may be multiple errors, we can only throw one of them at a time
if (count($errors) > 0) {
// there may be multiple errors, we can only throw one of them at a time
throw $errors[0];
}
}
@ -88,7 +104,7 @@ class DomHandler extends Handler
*
* @return array Array with exception objects for each modification
*/
public static function sanitizeAttr(DOMAttr $attr): array
public static function sanitizeAttr(DOMAttr $attr, array $options): array
{
// to be extended in child classes
return [];
@ -100,7 +116,7 @@ class DomHandler extends Handler
*
* @return array Array with exception objects for each modification
*/
public static function sanitizeElement(DOMElement $element): array
public static function sanitizeElement(DOMElement $element, array $options): array
{
// to be extended in child classes
return [];
@ -110,7 +126,7 @@ class DomHandler extends Handler
* Custom callback for additional doctype validation
* @internal
*/
public static function validateDoctype(DOMDocumentType $doctype): void
public static function validateDoctype(DOMDocumentType $doctype, array $options): void
{
// to be extended in child classes
}
@ -118,17 +134,29 @@ class DomHandler extends Handler
/**
* Returns the sanitization options for the handler
* (to be extended in child classes)
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*/
protected static function options(): array
protected static function options(bool $isExternal): array
{
return [
'allowedDataUris' => static::$allowedDataUris,
'allowedDomains' => static::$allowedDomains,
'allowedPIs' => static::$allowedPIs,
'attrCallback' => [static::class, 'sanitizeAttr'],
'doctypeCallback' => [static::class, 'validateDoctype'],
'elementCallback' => [static::class, 'sanitizeElement'],
$options = [
'allowedDataUris' => static::$allowedDataUris,
'allowedDomains' => static::$allowedDomains,
'allowHostRelativeUrls' => static::$allowHostRelativeUrls,
'allowedPIs' => static::$allowedPIs,
'attrCallback' => [static::class, 'sanitizeAttr'],
'doctypeCallback' => [static::class, 'validateDoctype'],
'elementCallback' => [static::class, 'sanitizeElement'],
];
// never allow host-relative URLs in external files as we
// cannot set a `<base>` element for them when accessed directly
if ($isExternal === true) {
$options['allowHostRelativeUrls'] = false;
}
return $options;
}
/**

View file

@ -21,8 +21,11 @@ abstract class Handler
{
/**
* Sanitizes the given string
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*/
abstract public static function sanitize(string $string): string;
abstract public static function sanitize(string $string, bool $isExternal = false): string;
/**
* Sanitizes the contents of a file by overwriting
@ -33,17 +36,21 @@ abstract class Handler
*/
public static function sanitizeFile(string $file): void
{
$sanitized = static::sanitize(static::readFile($file));
$content = static::readFile($file);
$sanitized = static::sanitize($content, isExternal: true);
F::write($file, $sanitized);
}
/**
* Validates file contents
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
* @throws \Kirby\Exception\Exception On other errors
*/
abstract public static function validate(string $string): void;
abstract public static function validate(string $string, bool $isExternal = false): void;
/**
* Validates the contents of a file
@ -54,7 +61,8 @@ abstract class Handler
*/
public static function validateFile(string $file): void
{
static::validate(static::readFile($file));
$content = static::readFile($file);
static::validate($content, isExternal: true);
}
/**

View file

@ -107,10 +107,13 @@ class Html extends DomHandler
/**
* Returns the sanitization options for the handler
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*/
protected static function options(): array
protected static function options(bool $isExternal): array
{
return array_merge(parent::options(), [
return array_merge(parent::options($isExternal), [
'allowedAttrPrefixes' => static::$allowedAttrPrefixes,
'allowedAttrs' => static::$allowedAttrs,
'allowedNamespaces' => [],

View file

@ -36,10 +36,10 @@ class Sane
* All registered handlers
*/
public static array $handlers = [
'html' => 'Kirby\Sane\Html',
'svg' => 'Kirby\Sane\Svg',
'svgz' => 'Kirby\Sane\Svgz',
'xml' => 'Kirby\Sane\Xml',
'html' => Html::class,
'svg' => Svg::class,
'svgz' => Svgz::class,
'xml' => Xml::class,
];
/**
@ -49,16 +49,19 @@ class Sane
*
* @throws \Kirby\Exception\NotFoundException If no handler was found and `$lazy` was set to `false`
*/
public static function handler(string $type, bool $lazy = false): Handler|null
{
public static function handler(
string $type,
bool $lazy = false
): Handler|null {
// normalize the type
$type = mb_strtolower($type);
// find a handler or alias
$alias = static::$aliases[$type] ?? null;
$handler =
static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null);
$handler = static::$handlers[$type] ?? null;
if ($alias = static::$aliases[$type] ?? null) {
$handler ??= static::$handlers[$alias] ?? null;
}
if (empty($handler) === false && class_exists($handler) === true) {
return new $handler();
@ -74,10 +77,13 @@ class Sane
/**
* Sanitizes the given string with the specified handler
* @since 3.6.0
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*/
public static function sanitize(string $string, string $type): string
public static function sanitize(string $string, string $type, bool $isExternal = false): string
{
return static::handler($type)->sanitize($string);
return static::handler($type)->sanitize($string, $isExternal);
}
/**
@ -96,8 +102,10 @@ class Sane
* @throws \Kirby\Exception\NotFoundException If the handler was not found
* @throws \Kirby\Exception\Exception On other errors
*/
public static function sanitizeFile(string $file, string|bool $typeLazy = false): void
{
public static function sanitizeFile(
string $file,
string|bool $typeLazy = false
): void {
if (is_string($typeLazy) === true) {
static::handler($typeLazy)->sanitizeFile($file);
return;
@ -126,13 +134,16 @@ class Sane
/**
* Validates file contents with the specified handler
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
* @throws \Kirby\Exception\NotFoundException If the handler was not found
* @throws \Kirby\Exception\Exception On other errors
*/
public static function validate(string $string, string $type): void
public static function validate(string $string, string $type, bool $isExternal = false): void
{
static::handler($type)->validate($string);
static::handler($type)->validate($string, $isExternal);
}
/**
@ -148,14 +159,18 @@ class Sane
* @throws \Kirby\Exception\NotFoundException If the handler was not found
* @throws \Kirby\Exception\Exception On other errors
*/
public static function validateFile(string $file, string|bool $typeLazy = false): void
{
public static function validateFile(
string $file,
string|bool $typeLazy = false
): void {
if (is_string($typeLazy) === true) {
static::handler($typeLazy)->validateFile($file);
return;
}
foreach (static::handlersForFile($file, $typeLazy === true) as $handler) {
$handlers = static::handlersForFile($file, $typeLazy === true);
foreach ($handlers as $handler) {
$handler->validateFile($file);
}
}
@ -167,8 +182,10 @@ class Sane
* @param bool $lazy If set to `true`, undefined handlers are skipped
* @return array<\Kirby\Sane\Handler>
*/
protected static function handlersForFile(string $file, bool $lazy = false): array
{
protected static function handlersForFile(
string $file,
bool $lazy = false
): array {
$handlers = $handlerClasses = [];
// all values that can be used for the handler search;
@ -180,7 +197,10 @@ class Sane
$handlerClass = $handler ? get_class($handler) : null;
// ensure that each handler class is only returned once
if ($handler && in_array($handlerClass, $handlerClasses) === false) {
if (
$handler &&
in_array($handlerClass, $handlerClasses) === false
) {
$handlers[] = $handler;
$handlerClasses[] = $handlerClass;
}

View file

@ -392,7 +392,7 @@ class Svg extends Xml
*
* @return array Array with exception objects for each modification
*/
public static function sanitizeAttr(DOMAttr $attr): array
public static function sanitizeAttr(DOMAttr $attr, array $options): array
{
$element = $attr->ownerElement;
$name = $attr->name;
@ -406,8 +406,9 @@ class Svg extends Xml
Str::startsWith($value, '#') === true
) {
// find the target (used element)
$id = str_replace('"', '', mb_substr($value, 1));
$target = (new DOMXPath($attr->ownerDocument))->query('//*[@id="' . $id . '"]')->item(0);
$id = str_replace('"', '', mb_substr($value, 1));
$path = new DOMXPath($attr->ownerDocument);
$target = $path->query('//*[@id="' . $id . '"]')->item(0);
// the target must not contain any other <use> elements
if (
@ -431,14 +432,14 @@ class Svg extends Xml
*
* @return array Array with exception objects for each modification
*/
public static function sanitizeElement(DOMElement $element): array
public static function sanitizeElement(DOMElement $element, array $options): array
{
$errors = [];
// check for URLs inside <style> elements
if ($element->tagName === 'style') {
foreach (Dom::extractUrls($element->textContent) as $url) {
if (Dom::isAllowedUrl($url, static::options()) !== true) {
if (Dom::isAllowedUrl($url, $options) !== true) {
$errors[] = new InvalidArgumentException(
'The URL is not allowed in the "style" element' .
' (around line ' . $element->getLineNo() . ')'
@ -455,7 +456,7 @@ class Svg extends Xml
* Custom callback for additional doctype validation
* @internal
*/
public static function validateDoctype(DOMDocumentType $doctype): void
public static function validateDoctype(DOMDocumentType $doctype, array $options): void
{
if (mb_strtolower($doctype->name) !== 'svg') {
throw new InvalidArgumentException('Invalid doctype');
@ -464,10 +465,13 @@ class Svg extends Xml
/**
* Returns the sanitization options for the handler
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*/
protected static function options(): array
protected static function options(bool $isExternal): array
{
return array_merge(parent::options(), [
return array_merge(parent::options($isExternal), [
'allowedAttrPrefixes' => static::$allowedAttrPrefixes,
'allowedAttrs' => static::$allowedAttrs,
'allowedNamespaces' => static::$allowedNamespaces,
@ -487,6 +491,7 @@ class Svg extends Xml
// basic validation before we continue sanitizing/validating
$rootName = $svg->document()->documentElement->nodeName;
if ($rootName !== 'svg') {
throw new InvalidArgumentException('The file is not a SVG (got <' . $rootName . '>)');
}

View file

@ -19,12 +19,15 @@ class Svgz extends Svg
/**
* Sanitizes the given string
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed or recompressed
*/
public static function sanitize(string $string): string
public static function sanitize(string $string, bool $isExternal = false): string
{
$string = static::uncompress($string);
$string = parent::sanitize($string);
$string = parent::sanitize($string, $isExternal);
$string = @gzencode($string);
if (is_string($string) !== true) {
@ -37,12 +40,16 @@ class Svgz extends Svg
/**
* Validates file contents
*
* @param bool $isExternal Whether the string is from an external file
* that may be accessed directly
*
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
*/
public static function validate(string $string): void
public static function validate(string $string, bool $isExternal = false): void
{
parent::validate(static::uncompress($string));
$string = static::uncompress($string);
parent::validate($string, $isExternal);
}
/**

View file

@ -26,14 +26,16 @@ class Xml extends DomHandler
*
* @return array Array with exception objects for each modification
*/
public static function sanitizeElement(DOMElement $element): array
public static function sanitizeElement(DOMElement $element, array $options): array
{
$errors = [];
// if we are validating an XML file, block all SVG and HTML namespaces
if (static::class === self::class) {
$simpleXmlElement = simplexml_import_dom($element);
foreach ($simpleXmlElement->getDocNamespaces(false, false) as $namespace => $value) {
$xml = simplexml_import_dom($element);
$namespaces = $xml->getDocNamespaces(false, false);
foreach ($namespaces as $namespace => $value) {
if (
Str::contains($value, 'html', true) === true ||
Str::contains($value, 'svg', true) === true
@ -54,7 +56,7 @@ class Xml extends DomHandler
* Custom callback for additional doctype validation
* @internal
*/
public static function validateDoctype(DOMDocumentType $doctype): void
public static function validateDoctype(DOMDocumentType $doctype, array $options): void
{
// if we are validating an XML file, block all SVG and HTML doctypes
if (