Upgrade to 3.5.4
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
"core"
|
"core"
|
||||||
],
|
],
|
||||||
"homepage": "https://getkirby.com",
|
"homepage": "https://getkirby.com",
|
||||||
"version": "3.5.3.1",
|
"version": "3.5.4",
|
||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
2
kirby/composer.lock
generated
2
kirby/composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "73d01b459246b298ff50d068f6b909af",
|
"content-hash": "70ee865c7f7cf618466ffb0f90bca1d8",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "claviska/simpleimage",
|
"name": "claviska/simpleimage",
|
||||||
|
@@ -85,6 +85,7 @@ class FileRules
|
|||||||
static::validFile($file, $upload->mime());
|
static::validFile($file, $upload->mime());
|
||||||
|
|
||||||
$upload->match($file->blueprint()->accept());
|
$upload->match($file->blueprint()->accept());
|
||||||
|
$upload->validateContents(true);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -133,6 +134,7 @@ class FileRules
|
|||||||
}
|
}
|
||||||
|
|
||||||
$upload->match($file->blueprint()->accept());
|
$upload->match($file->blueprint()->accept());
|
||||||
|
$upload->validateContents(true);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
50
kirby/src/Sane/Handler.php
Executable file
50
kirby/src/Sane/Handler.php
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kirby\Sane;
|
||||||
|
|
||||||
|
use Kirby\Exception\Exception;
|
||||||
|
use Kirby\Toolkit\F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base handler abstract,
|
||||||
|
* which needs to be extended to
|
||||||
|
* create valid sane handlers
|
||||||
|
*
|
||||||
|
* @package Kirby Sane
|
||||||
|
* @author Lukas Bestle <lukas@getkirby.com>
|
||||||
|
* @link https://getkirby.com
|
||||||
|
* @copyright Bastian Allgeier GmbH
|
||||||
|
* @license https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
abstract class Handler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validates file contents
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the contents of a file
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||||
|
* @throws \Kirby\Exception\Exception On other errors
|
||||||
|
*/
|
||||||
|
public static function validateFile(string $file): void
|
||||||
|
{
|
||||||
|
$contents = F::read($file);
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new Exception('The file "' . $file . '" does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
static::validate($contents);
|
||||||
|
}
|
||||||
|
}
|
128
kirby/src/Sane/Sane.php
Executable file
128
kirby/src/Sane/Sane.php
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kirby\Sane;
|
||||||
|
|
||||||
|
use Kirby\Exception\NotFoundException;
|
||||||
|
use Kirby\Toolkit\F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Sane` class validates that files
|
||||||
|
* don't contain potentially harmful contents.
|
||||||
|
* The class comes with handlers for `svg`, `svgz` and `xml`
|
||||||
|
* files for now, but can be extended and customized.
|
||||||
|
*
|
||||||
|
* @package Kirby Sane
|
||||||
|
* @author Lukas Bestle <lukas@getkirby.com>
|
||||||
|
* @link https://getkirby.com
|
||||||
|
* @copyright Bastian Allgeier GmbH
|
||||||
|
* @license https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
class Sane
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handler Type Aliases
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $aliases = [
|
||||||
|
'image/svg+xml' => 'svg',
|
||||||
|
'application/xml' => 'xml',
|
||||||
|
'text/xml' => 'xml',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All registered handlers
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $handlers = [
|
||||||
|
'svg' => 'Kirby\Sane\Svg',
|
||||||
|
'svgz' => 'Kirby\Sane\Svgz',
|
||||||
|
'xml' => 'Kirby\Sane\Xml',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler getter
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @param bool $lazy If set to `true`, `null` is returned for undefined handlers
|
||||||
|
* @return \Kirby\Sane\Handler|null
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\NotFoundException If no handler was found and `$lazy` was set to `false`
|
||||||
|
*/
|
||||||
|
public static function handler(string $type, bool $lazy = false)
|
||||||
|
{
|
||||||
|
// normalize the type
|
||||||
|
$type = mb_strtolower($type);
|
||||||
|
|
||||||
|
// find a handler or alias
|
||||||
|
$handler = static::$handlers[$type] ??
|
||||||
|
static::$handlers[static::$aliases[$type] ?? null] ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (empty($handler) === false && class_exists($handler) === true) {
|
||||||
|
return new $handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lazy === true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundException('Missing handler for type: "' . $type . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates file contents with the specified handler
|
||||||
|
*
|
||||||
|
* @param mixed $string
|
||||||
|
* @param string $type
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
static::handler($type)->validate($string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the contents of a file;
|
||||||
|
* the sane handlers are automatically chosen by
|
||||||
|
* the extension and MIME type if not specified
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @param string|bool $typeLazy Explicit handler type string,
|
||||||
|
* `true` for lazy autodetection or
|
||||||
|
* `false` for normal autodetection
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @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 validateFile(string $file, $typeLazy = false): void
|
||||||
|
{
|
||||||
|
if (is_string($typeLazy) === true) {
|
||||||
|
static::handler($typeLazy)->validateFile($file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [F::extension($file), F::mime($file)];
|
||||||
|
|
||||||
|
// execute all handlers, but each class only once for performance;
|
||||||
|
// filter out all empty options
|
||||||
|
$usedHandlers = [];
|
||||||
|
foreach (array_filter($options) as $option) {
|
||||||
|
$handler = static::handler($option, $typeLazy === true);
|
||||||
|
$handlerClass = $handler ? get_class($handler) : null;
|
||||||
|
|
||||||
|
if ($handler && in_array($handlerClass, $usedHandlers) === false) {
|
||||||
|
$handler->validateFile($file);
|
||||||
|
|
||||||
|
$usedHandlers[] = $handlerClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
485
kirby/src/Sane/Svg.php
Executable file
485
kirby/src/Sane/Svg.php
Executable file
@@ -0,0 +1,485 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kirby\Sane;
|
||||||
|
|
||||||
|
use DOMDocumentType;
|
||||||
|
use DOMNode;
|
||||||
|
use DOMNodeList;
|
||||||
|
use DOMXPath;
|
||||||
|
use Kirby\Exception\InvalidArgumentException;
|
||||||
|
use Kirby\Toolkit\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sane handler for SVG files
|
||||||
|
*
|
||||||
|
* @package Kirby Sane
|
||||||
|
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||||
|
* Lukas Bestle <lukas@getkirby.com>
|
||||||
|
* @link https://getkirby.com
|
||||||
|
* @copyright Bastian Allgeier GmbH
|
||||||
|
* @license https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
class Svg extends Xml
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Allow and block lists are inspired by DOMPurify
|
||||||
|
*
|
||||||
|
* @link https://github.com/cure53/DOMPurify
|
||||||
|
* @copyright 2015 Mario Heiderich
|
||||||
|
* @license https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
public static $allowedAttributes = [
|
||||||
|
'accent-height',
|
||||||
|
'accumulate',
|
||||||
|
'additive',
|
||||||
|
'alignment-baseline',
|
||||||
|
'ascent',
|
||||||
|
'attributeName',
|
||||||
|
'attributeType',
|
||||||
|
'azimuth',
|
||||||
|
'baseFrequency',
|
||||||
|
'baseline-shift',
|
||||||
|
'begin',
|
||||||
|
'bias',
|
||||||
|
'by',
|
||||||
|
'class',
|
||||||
|
'clip',
|
||||||
|
'clipPathUnits',
|
||||||
|
'clip-path',
|
||||||
|
'clip-rule',
|
||||||
|
'color',
|
||||||
|
'color-interpolation',
|
||||||
|
'color-interpolation-filters',
|
||||||
|
'color-profile',
|
||||||
|
'color-rendering',
|
||||||
|
'cx',
|
||||||
|
'cy',
|
||||||
|
'd',
|
||||||
|
'dx',
|
||||||
|
'dy',
|
||||||
|
'diffuseConstant',
|
||||||
|
'direction',
|
||||||
|
'display',
|
||||||
|
'divisor',
|
||||||
|
'dur',
|
||||||
|
'edgeMode',
|
||||||
|
'elevation',
|
||||||
|
'end',
|
||||||
|
'fill',
|
||||||
|
'fill-opacity',
|
||||||
|
'fill-rule',
|
||||||
|
'filter',
|
||||||
|
'filterUnits',
|
||||||
|
'flood-color',
|
||||||
|
'flood-opacity',
|
||||||
|
'font-family',
|
||||||
|
'font-size',
|
||||||
|
'font-size-adjust',
|
||||||
|
'font-stretch',
|
||||||
|
'font-style',
|
||||||
|
'font-variant',
|
||||||
|
'font-weight',
|
||||||
|
'fx',
|
||||||
|
'fy',
|
||||||
|
'g1',
|
||||||
|
'g2',
|
||||||
|
'glyph-name',
|
||||||
|
'glyphRef',
|
||||||
|
'gradientUnits',
|
||||||
|
'gradientTransform',
|
||||||
|
'height',
|
||||||
|
'href',
|
||||||
|
'id',
|
||||||
|
'image-rendering',
|
||||||
|
'in',
|
||||||
|
'in2',
|
||||||
|
'k',
|
||||||
|
'k1',
|
||||||
|
'k2',
|
||||||
|
'k3',
|
||||||
|
'k4',
|
||||||
|
'kerning',
|
||||||
|
'keyPoints',
|
||||||
|
'keySplines',
|
||||||
|
'keyTimes',
|
||||||
|
'lang',
|
||||||
|
'lengthAdjust',
|
||||||
|
'letter-spacing',
|
||||||
|
'kernelMatrix',
|
||||||
|
'kernelUnitLength',
|
||||||
|
'lighting-color',
|
||||||
|
'local',
|
||||||
|
'marker-end',
|
||||||
|
'marker-mid',
|
||||||
|
'marker-start',
|
||||||
|
'markerHeight',
|
||||||
|
'markerUnits',
|
||||||
|
'markerWidth',
|
||||||
|
'maskContentUnits',
|
||||||
|
'maskUnits',
|
||||||
|
'max',
|
||||||
|
'mask',
|
||||||
|
'media',
|
||||||
|
'method',
|
||||||
|
'mode',
|
||||||
|
'min',
|
||||||
|
'name',
|
||||||
|
'numOctaves',
|
||||||
|
'offset',
|
||||||
|
'operator',
|
||||||
|
'opacity',
|
||||||
|
'order',
|
||||||
|
'orient',
|
||||||
|
'orientation',
|
||||||
|
'origin',
|
||||||
|
'overflow',
|
||||||
|
'paint-order',
|
||||||
|
'path',
|
||||||
|
'pathLength',
|
||||||
|
'patternContentUnits',
|
||||||
|
'patternTransform',
|
||||||
|
'patternUnits',
|
||||||
|
'points',
|
||||||
|
'preserveAlpha',
|
||||||
|
'preserveAspectRatio',
|
||||||
|
'primitiveUnits',
|
||||||
|
'r',
|
||||||
|
'rx',
|
||||||
|
'ry',
|
||||||
|
'radius',
|
||||||
|
'refX',
|
||||||
|
'refY',
|
||||||
|
'repeatCount',
|
||||||
|
'repeatDur',
|
||||||
|
'restart',
|
||||||
|
'result',
|
||||||
|
'rotate',
|
||||||
|
'scale',
|
||||||
|
'seed',
|
||||||
|
'shape-rendering',
|
||||||
|
'specularConstant',
|
||||||
|
'specularExponent',
|
||||||
|
'spreadMethod',
|
||||||
|
'startOffset',
|
||||||
|
'stdDeviation',
|
||||||
|
'stitchTiles',
|
||||||
|
'stop-color',
|
||||||
|
'stop-opacity',
|
||||||
|
'stroke-dasharray',
|
||||||
|
'stroke-dashoffset',
|
||||||
|
'stroke-linecap',
|
||||||
|
'stroke-linejoin',
|
||||||
|
'stroke-miterlimit',
|
||||||
|
'stroke-opacity',
|
||||||
|
'stroke',
|
||||||
|
'stroke-width',
|
||||||
|
'style',
|
||||||
|
'surfaceScale',
|
||||||
|
'systemLanguage',
|
||||||
|
'tabindex',
|
||||||
|
'targetX',
|
||||||
|
'targetY',
|
||||||
|
'transform',
|
||||||
|
'text-anchor',
|
||||||
|
'text-decoration',
|
||||||
|
'text-rendering',
|
||||||
|
'textLength',
|
||||||
|
'type',
|
||||||
|
'u1',
|
||||||
|
'u2',
|
||||||
|
'unicode',
|
||||||
|
'values',
|
||||||
|
'viewBox',
|
||||||
|
'visibility',
|
||||||
|
'version',
|
||||||
|
'vert-adv-y',
|
||||||
|
'vert-origin-x',
|
||||||
|
'vert-origin-y',
|
||||||
|
'width',
|
||||||
|
'word-spacing',
|
||||||
|
'wrap',
|
||||||
|
'writing-mode',
|
||||||
|
'xChannelSelector',
|
||||||
|
'yChannelSelector',
|
||||||
|
'x',
|
||||||
|
'x1',
|
||||||
|
'x2',
|
||||||
|
'xlink:href',
|
||||||
|
'y',
|
||||||
|
'y1',
|
||||||
|
'y2',
|
||||||
|
'z',
|
||||||
|
'zoomAndPan',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $allowedElements = [
|
||||||
|
'svg',
|
||||||
|
'a',
|
||||||
|
'altGlyph',
|
||||||
|
'altGlyphDef',
|
||||||
|
'altGlyphItem',
|
||||||
|
'animateColor',
|
||||||
|
'animateMotion',
|
||||||
|
'animateTransform',
|
||||||
|
'circle',
|
||||||
|
'clipPath',
|
||||||
|
'defs',
|
||||||
|
'desc',
|
||||||
|
'ellipse',
|
||||||
|
'filter',
|
||||||
|
'font',
|
||||||
|
'g',
|
||||||
|
'glyph',
|
||||||
|
'glyphRef',
|
||||||
|
'hkern',
|
||||||
|
'image',
|
||||||
|
'line',
|
||||||
|
'linearGradient',
|
||||||
|
'marker',
|
||||||
|
'mask',
|
||||||
|
'metadata',
|
||||||
|
'mpath',
|
||||||
|
'path',
|
||||||
|
'pattern',
|
||||||
|
'polygon',
|
||||||
|
'polyline',
|
||||||
|
'radialGradient',
|
||||||
|
'rect',
|
||||||
|
'stop',
|
||||||
|
'style',
|
||||||
|
'switch',
|
||||||
|
'symbol',
|
||||||
|
'text',
|
||||||
|
'textPath',
|
||||||
|
'title',
|
||||||
|
'tref',
|
||||||
|
'tspan',
|
||||||
|
'use',
|
||||||
|
'view',
|
||||||
|
'vkern',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $allowedFilters = [
|
||||||
|
'feBlend',
|
||||||
|
'feColorMatrix',
|
||||||
|
'feComponentTransfer',
|
||||||
|
'feComposite',
|
||||||
|
'feConvolveMatrix',
|
||||||
|
'feDiffuseLighting',
|
||||||
|
'feDisplacementMap',
|
||||||
|
'feDistantLight',
|
||||||
|
'feFlood',
|
||||||
|
'feFuncA',
|
||||||
|
'feFuncB',
|
||||||
|
'feFuncG',
|
||||||
|
'feFuncR',
|
||||||
|
'feGaussianBlur',
|
||||||
|
'feMerge',
|
||||||
|
'feMergeNode',
|
||||||
|
'feMorphology',
|
||||||
|
'feOffset',
|
||||||
|
'fePointLight',
|
||||||
|
'feSpecularLighting',
|
||||||
|
'feSpotLight',
|
||||||
|
'feTile',
|
||||||
|
'feTurbulence',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static $allowedNamespaces = [
|
||||||
|
'xmlns' => 'http://www.w3.org/2000/svg',
|
||||||
|
'xmlns:svg' => 'http://www.w3.org/2000/svg',
|
||||||
|
'xmlns:xlink' => 'http://www.w3.org/1999/xlink'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMPORTANT: Use lower-case names here because
|
||||||
|
* of the case-insensitive matching
|
||||||
|
*/
|
||||||
|
public static $disallowedElements = [
|
||||||
|
'animate',
|
||||||
|
'color-profile',
|
||||||
|
'cursor',
|
||||||
|
'discard',
|
||||||
|
'fedropshadow',
|
||||||
|
'feimage',
|
||||||
|
'font-face',
|
||||||
|
'font-face-format',
|
||||||
|
'font-face-name',
|
||||||
|
'font-face-src',
|
||||||
|
'font-face-uri',
|
||||||
|
'foreignobject',
|
||||||
|
'hatch',
|
||||||
|
'hatchpath',
|
||||||
|
'mesh',
|
||||||
|
'meshgradient',
|
||||||
|
'meshpatch',
|
||||||
|
'meshrow',
|
||||||
|
'missing-glyph',
|
||||||
|
'script',
|
||||||
|
'set',
|
||||||
|
'solidcolor',
|
||||||
|
'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates file contents
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||||
|
*/
|
||||||
|
public static function validate(string $string): void
|
||||||
|
{
|
||||||
|
$svg = static::parse($string);
|
||||||
|
|
||||||
|
$rootName = $svg->documentElement->nodeName;
|
||||||
|
if ($rootName !== 'svg') {
|
||||||
|
throw new InvalidArgumentException('The file is not a SVG (got <' . $rootName . '>)');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::validateDom($svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the attributes of an element
|
||||||
|
*
|
||||||
|
* @param \DOMXPath $xPath
|
||||||
|
* @param \DOMNode $element
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If any of the attributes is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateAttrs(DOMXPath $xPath, DOMNode $element): void
|
||||||
|
{
|
||||||
|
$elementName = $element->nodeName;
|
||||||
|
|
||||||
|
foreach ($element->attributes ?? [] as $attr) {
|
||||||
|
$attrName = $attr->nodeName;
|
||||||
|
$attrValue = $attr->nodeValue;
|
||||||
|
|
||||||
|
// allow all aria and data attributes
|
||||||
|
$beginning = mb_substr($attrName, 0, 5);
|
||||||
|
if ($beginning === 'aria-' || $beginning === 'data-') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($attrName, static::$allowedAttributes) !== true) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The "' . $attrName . '" attribute (line ' .
|
||||||
|
$attr->getLineNo() . ') is not allowed in SVGs'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// block nested <use> elements ("Billion Laughs" DoS attack)
|
||||||
|
if (
|
||||||
|
$elementName === 'use' &&
|
||||||
|
Str::contains($attrName, 'href') !== false &&
|
||||||
|
Str::startsWith($attrValue, '#') === true
|
||||||
|
) {
|
||||||
|
// find the target (used element)
|
||||||
|
$id = str_replace('"', '', mb_substr($attrValue, 1));
|
||||||
|
$target = $xPath->query('//*[@id="' . $id . '"]')->item(0);
|
||||||
|
|
||||||
|
// the target must not contain any other <use> elements
|
||||||
|
if (
|
||||||
|
is_a($target, 'DOMElement') === true &&
|
||||||
|
$target->getElementsByTagName('use')->count() > 0
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Nested "use" elements are not allowed in SVGs (used in line ' .
|
||||||
|
$element->getLineNo() . ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate `xmlns` attributes as well, which can only
|
||||||
|
// be properly extracted using SimpleXML
|
||||||
|
if (is_a($element, 'DOMElement') === true) {
|
||||||
|
$simpleXmlElement = simplexml_import_dom($element);
|
||||||
|
foreach ($simpleXmlElement->getDocNamespaces(false, false) as $namespace => $value) {
|
||||||
|
$namespace = 'xmlns' . ($namespace ? ':' . $namespace : '');
|
||||||
|
|
||||||
|
// check if the namespace is allowlisted
|
||||||
|
if (
|
||||||
|
isset(static::$allowedNamespaces[$namespace]) !== true ||
|
||||||
|
static::$allowedNamespaces[$namespace] !== $value
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The namespace "' . $namespace . '" (around line ' .
|
||||||
|
$element->getLineNo() . ') is not allowed or has an invalid value'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::validateAttrs($xPath, $element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the doctype if present
|
||||||
|
*
|
||||||
|
* @param \DOMDocumentType $doctype
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the doctype is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateDoctype(DOMDocumentType $doctype): void
|
||||||
|
{
|
||||||
|
if (mb_strtolower($doctype->name) !== 'svg') {
|
||||||
|
throw new InvalidArgumentException('Invalid doctype');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::validateDoctype($doctype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates all given DOM elements and their attributes
|
||||||
|
*
|
||||||
|
* @param \DOMXPath $xPath
|
||||||
|
* @param \DOMNodeList $elements
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If any of the elements is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateElements(DOMXPath $xPath, DOMNodeList $elements): void
|
||||||
|
{
|
||||||
|
$allowedElements = array_merge(static::$allowedElements, static::$allowedFilters);
|
||||||
|
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
$elementName = $element->nodeName;
|
||||||
|
$elementNameLower = mb_strtolower($elementName);
|
||||||
|
|
||||||
|
// check for block-listed elements
|
||||||
|
if (in_array($elementNameLower, static::$disallowedElements) === true) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The "' . $elementName . '" element (line ' .
|
||||||
|
$element->getLineNo() . ') is not allowed in SVGs'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for allow-listed elements
|
||||||
|
if (in_array($elementName, $allowedElements) === false) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The "' . $elementName . '" element (line ' .
|
||||||
|
$element->getLineNo() . ') is not allowed in SVGs'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for URLs inside <style> elements
|
||||||
|
if ($elementName === 'style') {
|
||||||
|
foreach (static::extractUrls($element->textContent) as $url) {
|
||||||
|
if (static::isAllowedUrl($url) !== true) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The URL is not allowed in the <style> element' .
|
||||||
|
' (around line ' . $element->getLineNo() . ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::validateElements($xPath, $elements);
|
||||||
|
}
|
||||||
|
}
|
38
kirby/src/Sane/Svgz.php
Executable file
38
kirby/src/Sane/Svgz.php
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kirby\Sane;
|
||||||
|
|
||||||
|
use Kirby\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sane handler for gzip-compressed SVGZ files
|
||||||
|
*
|
||||||
|
* @package Kirby Sane
|
||||||
|
* @author Lukas Bestle <lukas@getkirby.com>
|
||||||
|
* @link https://getkirby.com
|
||||||
|
* @copyright Bastian Allgeier GmbH
|
||||||
|
* @license https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
class Svgz extends Svg
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validates file contents
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||||
|
*/
|
||||||
|
public static function validate(string $string): void
|
||||||
|
{
|
||||||
|
// only support uncompressed files up to 10 MB to
|
||||||
|
// prevent gzip bombs from crashing the process
|
||||||
|
$uncompressed = @gzdecode($string, 10000000);
|
||||||
|
|
||||||
|
if (is_string($uncompressed) !== true) {
|
||||||
|
throw new InvalidArgumentException('Could not uncompress gzip data');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::validate($uncompressed);
|
||||||
|
}
|
||||||
|
}
|
338
kirby/src/Sane/Xml.php
Executable file
338
kirby/src/Sane/Xml.php
Executable file
@@ -0,0 +1,338 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kirby\Sane;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMDocumentType;
|
||||||
|
use DOMNode;
|
||||||
|
use DOMNodeList;
|
||||||
|
use DOMXPath;
|
||||||
|
use Kirby\Exception\InvalidArgumentException;
|
||||||
|
use Kirby\Toolkit\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sane handler for XML files
|
||||||
|
*
|
||||||
|
* @package Kirby Sane
|
||||||
|
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||||
|
* Lukas Bestle <lukas@getkirby.com>
|
||||||
|
* @link https://getkirby.com
|
||||||
|
* @copyright Bastian Allgeier GmbH
|
||||||
|
* @license https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
class Xml extends Handler
|
||||||
|
{
|
||||||
|
public static $allowedDataAttrs = [
|
||||||
|
'data:image/png',
|
||||||
|
'data:image/gif',
|
||||||
|
'data:image/jpg',
|
||||||
|
'data:image/jpe',
|
||||||
|
'data:image/pjp',
|
||||||
|
'data:img/png',
|
||||||
|
'data:img/gif',
|
||||||
|
'data:img/jpg',
|
||||||
|
'data:img/jpe',
|
||||||
|
'data:img/pjp',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $allowedDomains = [];
|
||||||
|
|
||||||
|
public static $allowedPIs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates file contents
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||||
|
*/
|
||||||
|
public static function validate(string $string): void
|
||||||
|
{
|
||||||
|
$xml = static::parse($string);
|
||||||
|
|
||||||
|
static::validateDom($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all URLs wrapped in a url() wrapper. E.g. for style attributes.
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected static function extractUrls(string $value): array
|
||||||
|
{
|
||||||
|
$count = preg_match_all(
|
||||||
|
'!url\(\s*[\'"]?(.*?)[\'"]?\s*\)!i',
|
||||||
|
static::trim($value),
|
||||||
|
$matches,
|
||||||
|
PREG_PATTERN_ORDER
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_int($count) === true && $count > 0) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the URL is acceptable for href attributes
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected static function isAllowedUrl(string $url): bool
|
||||||
|
{
|
||||||
|
$url = mb_strtolower($url);
|
||||||
|
|
||||||
|
// allow empty URL values
|
||||||
|
if (empty($url) === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow URLs that point to fragments inside the file
|
||||||
|
// as well as site-internal URLs
|
||||||
|
if (in_array(mb_substr($url, 0, 1), ['#', '/']) === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow specific HTTP(S) URLs
|
||||||
|
if (
|
||||||
|
Str::startsWith($url, 'http://') === true ||
|
||||||
|
Str::startsWith($url, 'https://') === true
|
||||||
|
) {
|
||||||
|
$hostname = parse_url($url, PHP_URL_HOST);
|
||||||
|
|
||||||
|
if (in_array($hostname, static::$allowedDomains) === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow listed data URIs
|
||||||
|
foreach (static::$allowedDataAttrs as $dataAttr) {
|
||||||
|
if (Str::startsWith($url, $dataAttr) === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to parse an XML string
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return \DOMDocument
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||||
|
*/
|
||||||
|
protected static function parse(string $string)
|
||||||
|
{
|
||||||
|
$xml = new DOMDocument();
|
||||||
|
$xml->preserveWhiteSpace = false;
|
||||||
|
$xml->strictErrorChecking = false;
|
||||||
|
|
||||||
|
$loaderSetting = null;
|
||||||
|
if (\PHP_VERSION_ID < 80000) {
|
||||||
|
// prevent loading external entities to protect against XXE attacks;
|
||||||
|
// only needed for PHP versions before 8.0 (the function was deprecated
|
||||||
|
// as the disabled state is the new default in PHP 8.0+)
|
||||||
|
$loaderSetting = libxml_disable_entity_loader(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch to "user error handling"
|
||||||
|
$intErrorsSetting = libxml_use_internal_errors(true);
|
||||||
|
|
||||||
|
$load = $xml->loadXML($string);
|
||||||
|
|
||||||
|
if (\PHP_VERSION_ID < 80000) {
|
||||||
|
// ensure that we don't alter global state by
|
||||||
|
// resetting the original value
|
||||||
|
libxml_disable_entity_loader($loaderSetting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get one error for use below and reset the global state
|
||||||
|
$error = libxml_get_last_error();
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($intErrorsSetting);
|
||||||
|
|
||||||
|
if ($load !== true) {
|
||||||
|
$message = 'The file could not be parsed';
|
||||||
|
|
||||||
|
if ($error !== false) {
|
||||||
|
$message .= ': ' . $error->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException([
|
||||||
|
'fallback' => $message,
|
||||||
|
'details' => compact('error')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes invisible ASCII characters from the value
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function trim(string $value): string
|
||||||
|
{
|
||||||
|
return trim(preg_replace('/[^ -~]/u', '', $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the attributes of an element
|
||||||
|
*
|
||||||
|
* @param \DOMXPath $xPath
|
||||||
|
* @param \DOMNode $element
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If any of the attributes is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateAttrs(DOMXPath $xPath, DOMNode $element): void
|
||||||
|
{
|
||||||
|
$elementName = $element->nodeName;
|
||||||
|
|
||||||
|
foreach ($element->attributes ?? [] as $attr) {
|
||||||
|
$attrName = $attr->nodeName;
|
||||||
|
$attrValue = $attr->nodeValue;
|
||||||
|
|
||||||
|
if (Str::contains($attrName, 'href') !== false) {
|
||||||
|
if (static::isAllowedUrl($attrValue) !== true) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The URL is not allowed in attribute: ' . $attrName .
|
||||||
|
' (line ' . $attr->getLineNo() . ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// check for unwanted URLs in other attributes
|
||||||
|
foreach (static::extractUrls($attrValue) as $url) {
|
||||||
|
if (static::isAllowedUrl($url) !== true) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The URL is not allowed in attribute: ' . $attrName .
|
||||||
|
' (line ' . $attr->getLineNo() . ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are validating an XML file, block
|
||||||
|
// all SVG and HTML namespaces
|
||||||
|
if (static::class === self::class && is_a($element, 'DOMElement') === true) {
|
||||||
|
$simpleXmlElement = simplexml_import_dom($element);
|
||||||
|
foreach ($simpleXmlElement->getDocNamespaces(false, false) as $namespace => $value) {
|
||||||
|
if (
|
||||||
|
Str::contains($value, 'html', true) === true ||
|
||||||
|
Str::contains($value, 'svg', true) === true
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The namespace is not allowed in XML files' .
|
||||||
|
' (around line ' . $element->getLineNo() . ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the doctype if present
|
||||||
|
*
|
||||||
|
* @param \DOMDocumentType $doctype
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the doctype is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateDoctype(DOMDocumentType $doctype): void
|
||||||
|
{
|
||||||
|
// if we are validating an XML file, block
|
||||||
|
// all SVG and HTML doctypes
|
||||||
|
if (
|
||||||
|
static::class === self::class &&
|
||||||
|
(
|
||||||
|
Str::contains($doctype->name, 'html', true) === true ||
|
||||||
|
Str::contains($doctype->name, 'svg', true) === true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('The doctype is not allowed in XML files');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($doctype->publicId) === false || empty($doctype->systemId) === false) {
|
||||||
|
throw new InvalidArgumentException('The doctype must not reference external files');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($doctype->internalSubset) === false) {
|
||||||
|
throw new InvalidArgumentException('The doctype must not define a subset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a DOMDocument tree
|
||||||
|
*
|
||||||
|
* @param \DOMDocument $string
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If the document didn't pass validation
|
||||||
|
*/
|
||||||
|
protected static function validateDom(DOMDocument $xml): void
|
||||||
|
{
|
||||||
|
foreach ($xml->childNodes as $child) {
|
||||||
|
if (is_a($child, 'DOMDocumentType') === true) {
|
||||||
|
static::validateDoctype($child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate all processing instructions like <?xml-stylesheet
|
||||||
|
$xPath = new DOMXPath($xml);
|
||||||
|
$pis = $xPath->query('//processing-instruction()');
|
||||||
|
static::validateProcessingInstructions($pis);
|
||||||
|
|
||||||
|
// validate all elements in the document tree
|
||||||
|
$elements = $xml->getElementsByTagName('*');
|
||||||
|
static::validateElements($xPath, $elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates all given DOM elements and their attributes
|
||||||
|
*
|
||||||
|
* @param \DOMXPath $xPath
|
||||||
|
* @param \DOMNodeList $elements
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If any of the elements is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateElements(DOMXPath $xPath, DOMNodeList $elements): void
|
||||||
|
{
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
// check for allow-listed attributes
|
||||||
|
static::validateAttrs($xPath, $element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the values of all given processing instructions
|
||||||
|
*
|
||||||
|
* @param \DOMNodeList $elements
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Kirby\Exception\InvalidArgumentException If any of the processing instructions is not valid
|
||||||
|
*/
|
||||||
|
protected static function validateProcessingInstructions(DOMNodeList $elements): void
|
||||||
|
{
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
$elementName = $element->nodeName;
|
||||||
|
|
||||||
|
// check for allow-listed processing instructions
|
||||||
|
if (in_array($elementName, static::$allowedPIs) === false) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'The "' . $elementName . '" processing instruction (line ' .
|
||||||
|
$element->getLineNo() . ') is not allowed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@
|
|||||||
namespace Kirby\Toolkit;
|
namespace Kirby\Toolkit;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Kirby\Sane\Sane;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flexible File object with a set of helpful
|
* Flexible File object with a set of helpful
|
||||||
@@ -323,6 +324,23 @@ class File
|
|||||||
return F::type($this->root);
|
return F::type($this->root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the file contents depending on the file type
|
||||||
|
*
|
||||||
|
* @param string|bool $typeLazy Explicit sane handler type string,
|
||||||
|
* `true` for lazy autodetection or
|
||||||
|
* `false` for normal autodetection
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @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 function validateContents($typeLazy = false): void
|
||||||
|
{
|
||||||
|
Sane::validateFile($this->root(), $typeLazy);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes content to the file
|
* Writes content to the file
|
||||||
*
|
*
|
||||||
|
8
kirby/vendor/composer/InstalledVersions.php
vendored
8
kirby/vendor/composer/InstalledVersions.php
vendored
@@ -14,8 +14,8 @@ class InstalledVersions
|
|||||||
private static $installed = array (
|
private static $installed = array (
|
||||||
'root' =>
|
'root' =>
|
||||||
array (
|
array (
|
||||||
'pretty_version' => '3.5.3.1',
|
'pretty_version' => '3.5.4',
|
||||||
'version' => '3.5.3.1',
|
'version' => '3.5.4.0',
|
||||||
'aliases' =>
|
'aliases' =>
|
||||||
array (
|
array (
|
||||||
),
|
),
|
||||||
@@ -44,8 +44,8 @@ private static $installed = array (
|
|||||||
),
|
),
|
||||||
'getkirby/cms' =>
|
'getkirby/cms' =>
|
||||||
array (
|
array (
|
||||||
'pretty_version' => '3.5.3.1',
|
'pretty_version' => '3.5.4',
|
||||||
'version' => '3.5.3.1',
|
'version' => '3.5.4.0',
|
||||||
'aliases' =>
|
'aliases' =>
|
||||||
array (
|
array (
|
||||||
),
|
),
|
||||||
|
8
kirby/vendor/composer/installed.php
vendored
8
kirby/vendor/composer/installed.php
vendored
@@ -1,8 +1,8 @@
|
|||||||
<?php return array (
|
<?php return array (
|
||||||
'root' =>
|
'root' =>
|
||||||
array (
|
array (
|
||||||
'pretty_version' => '3.5.3.1',
|
'pretty_version' => '3.5.4',
|
||||||
'version' => '3.5.3.1',
|
'version' => '3.5.4.0',
|
||||||
'aliases' =>
|
'aliases' =>
|
||||||
array (
|
array (
|
||||||
),
|
),
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
),
|
),
|
||||||
'getkirby/cms' =>
|
'getkirby/cms' =>
|
||||||
array (
|
array (
|
||||||
'pretty_version' => '3.5.3.1',
|
'pretty_version' => '3.5.4',
|
||||||
'version' => '3.5.3.1',
|
'version' => '3.5.4.0',
|
||||||
'aliases' =>
|
'aliases' =>
|
||||||
array (
|
array (
|
||||||
),
|
),
|
||||||
|
Reference in New Issue
Block a user