Merge branch 'release/1.7.19'

This commit is contained in:
Andy Miller
2021-08-31 12:21:42 -06:00
30 changed files with 2525 additions and 239 deletions

View File

@@ -13,5 +13,5 @@ indent_size = 4
trim_trailing_whitespace = true
# 2 space indentation
[*.{yaml,yml}]
[*.{yaml,yml,vue,js,css}]
indent_size = 2

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ tests/cache/*
tests/error.log
system/templates/testing/*
/user/config/versions.yaml
/user/cli/config/security.yaml

View File

@@ -1,3 +1,28 @@
# v1.7.19
## 08/31/2021
1. [](#new)
* Include active form and request in `onPageTask` and `onPageAction` events (defaults to `null`)
* Added `UserObject::$authorizeCallable` to allow `$user->authorize()` customization
2. [](#improved)
* Added meta support for `UploadedFile` class
* Added support for multiple mime-types per file extension [#3422](https://github.com/getgrav/grav/issues/3422)
* Added `setCurrent()` method to Page Collection [#3398](https://github.com/getgrav/grav/pull/3398)
* Initialize `$grav['uri']` before session
3. [](#bugfix)
* Fixed `Warning: Undefined array key "SERVER_SOFTWARE" in index.php` [#3408](https://github.com/getgrav/grav/issues/3408)
* Fixed error in `loadDirectoryConfig()` if configuration hasn't been saved [#3409](https://github.com/getgrav/grav/issues/3409)
* Fixed GPM not using non-standard cache path [#3410](https://github.com/getgrav/grav/issues/3410)
* Fixed broken `environment://` stream when it doesn't have configuration
* Fixed `Flex Object` missing key field value when using `FolderStorage`
* Fixed broken Twig try tag when catch has not been defined or is empty
* Fixed `FlexForm` serialization
* Fixed form validation for numeric values in PHP 8
* Fixed `flex-options@` in blueprints duplicating items in array
* Fixed wrong form issue with flex objects after cache clear
* Fixed Flex object types not implementing `MediaInterface`
* Fixed issue with `svgImageFunction()` that was causing broken output
# v1.7.18
## 07/19/2021

View File

@@ -1,7 +1,6 @@
# ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
[![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org)
[![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors)

366
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
}
if (PHP_SAPI === 'cli-server') {
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'], 'symfony') !== false;
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");

View File

@@ -28,6 +28,10 @@ types:
type: image
thumb: media/thumb-webp.png
mime: image/webp
avif:
type: image
thumb: media/thumb.png
mime: image/avif
gif:
type: animated
thumb: media/thumb-gif.png
@@ -91,7 +95,7 @@ types:
aif:
type: audio
thumb: media/thumb-aif.png
mime: audio/aif
mime: audio/aiff
txt:
type: file
thumb: media/thumb-txt.png
@@ -207,7 +211,7 @@ types:
js:
type: file
thumb: media/thumb-js.png
mime: application/javascript
mime: text/javascript
json:
type: file
thumb: media/thumb-json.png

1986
system/config/mime.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.18');
define('GRAV_VERSION', '1.7.19');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);

View File

@@ -41,6 +41,9 @@ class Setup extends Data
*/
public static $environment;
/** @var string */
public static $securityFile = 'config://security.yaml';
/** @var array */
protected $streams = [
'user' => [
@@ -390,12 +393,15 @@ class Setup extends Data
if (!$locator->findResource('environment://config', true)) {
// If environment does not have its own directory, remove it from the lookup.
$this->set('streams.schemes.environment.prefixes', ['config' => []]);
$prefixes = $this->get('streams.schemes.environment.prefixes');
$prefixes['config'] = [];
$this->set('streams.schemes.environment.prefixes', $prefixes);
$this->initializeLocator($locator);
}
// Create security.yaml if it doesn't exist.
$filename = $locator->findResource('config://security.yaml', true, true);
$filename = $locator->findResource(static::$securityFile, true, true);
$security_file = CompiledYamlFile::instance($filename);
$security_content = (array)$security_file->content();

View File

@@ -519,17 +519,30 @@ class Validation
return false;
}
if (isset($params['min']) && $value < $params['min']) {
return false;
$value = (float)$value;
$min = 0;
if (isset($params['min'])) {
$min = (float)$params['min'];
if ($value < $min) {
return false;
}
}
if (isset($params['max']) && $value > $params['max']) {
return false;
if (isset($params['max'])) {
$max = (float)$params['max'];
if ($value > $max) {
return false;
}
}
$min = $params['min'] ?? 0;
if (isset($params['step'])) {
$step = (float)$params['step'];
return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
return fmod($value - $min, $step) === 0.0;
}
return true;
}
/**

View File

@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexObjectTrait;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use function is_array;
@@ -21,7 +22,7 @@ use function is_array;
*
* @package Grav\Common\Flex
*/
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
{
use FlexGravTrait;
use FlexObjectTrait;

View File

@@ -192,6 +192,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
/**
* Set current page.
*/
public function setCurrent(string $path): void
{
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
/**
* Return previous item.
*

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Grav\Common\Flex\Types\Users;
use Closure;
use Countable;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
@@ -75,6 +76,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
use UserTrait;
use UserObjectLegacyTrait;
/** @var Closure|null */
static public $authorizeCallable;
/** @var array|null */
protected $_uploads_original;
/** @var FileInterface|null */
@@ -259,6 +263,15 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
}
$authorizeCallable = static::$authorizeCallable;
if ($authorizeCallable instanceof Closure) {
$authorizeCallable->bindTo($this);
$authorized = $authorizeCallable($action, $scope);
if (is_bool($authorized)) {
return $authorized;
}
}
// Check user access.
$access = $this->getAccess();
$authorized = $access->authorize($action, $scope);

View File

@@ -60,7 +60,7 @@ class GPM extends Iterator
{
parent::__construct();
Folder::create(GRAV_ROOT . '/cache/gpm');
Folder::create(CACHE_DIR . '/gpm');
$this->cache = [];
$this->installed = new Local\Packages();

View File

@@ -20,11 +20,13 @@ use Grav\Common\Security;
use Grav\Common\Utils;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Mime\MimeTypes;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use function dirname;
use function in_array;
/**
* Implements media upload and delete functionality.
@@ -179,16 +181,20 @@ trait MediaUploadTrait
}
}
$grav = Grav::instance();
/** @var MimeTypes $mimeChecker */
$mimeChecker = $grav['mime'];
// Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
$accepted = false;
$errors = [];
// Do not trust mime type sent by the browser.
$mime = Utils::getMimeByFilename($filename);
$mimeTest = $metadata['mime'] ?? $mime;
if ($mime !== $mimeTest) {
$mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
$validExtensions = $mimeChecker->getExtensions($mime);
if (!in_array($extension, $validExtensions, true)) {
throw new RuntimeException('The mime type does not match to file extension', 400);
}
$accepted = false;
$errors = [];
foreach ((array)$settings['accept'] as $type) {
// Force acceptance of any file when star notation
if ($type === '*') {
@@ -418,6 +424,17 @@ trait MediaUploadTrait
$uploadedFile->moveTo($filepath);
}
/**
* Get upload settings.
*
* @param array|null $settings Form field specific settings (override).
* @return array
*/
public function getUploadSettings(?array $settings = null): array
{
return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
}
/**
* Internal logic to copy file.
*
@@ -604,17 +621,6 @@ trait MediaUploadTrait
}
}
/**
* Get upload settings.
*
* @param array|null $settings Form field specific settings (override).
* @return array
*/
protected function getUploadSettings(?array $settings = null): array
{
return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
}
/**
* @param string $filename
* @param string $path

View File

@@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface
return $this;
}
/**
* Set current page.
*/
public function setCurrent(string $path): void
{
reset($this->items);
while (($key = key($this->items)) !== null && $key !== $path) {
next($this->items);
}
}
/**
* Returns current page.
*

View File

@@ -105,12 +105,12 @@ class InitializeProcessor extends ProcessorBase
// TODO: remove in 2.0.
$this->container['accounts'];
// Initialize session.
$this->initializeSession($config);
// Initialize URI (uses session, see issue #3269).
$this->initializeUri($config);
// Initialize session.
$this->initializeSession($config);
// Grav may return redirect response right away.
$redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
if ($redirectCode) {

View File

@@ -10,6 +10,7 @@
namespace Grav\Common\Processors;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Plugin\Form\Forms;
use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -65,12 +66,18 @@ class PagesProcessor extends ProcessorBase
$task = $this->container['task'];
$action = $this->container['action'];
/** @var Forms $forms */
$forms = $this->container['forms'] ?? null;
$form = $forms ? $forms->getActiveForm() : null;
$options = ['page' => $page, 'form' => $form, 'request' => $request];
if ($task) {
$event = new Event(['task' => $task, 'page' => $page]);
$event = new Event(['task' => $task] + $options);
$this->container->fireEvent('onPageTask', $event);
$this->container->fireEvent('onPageTask.' . $task, $event);
} elseif ($action) {
$event = new Event(['action' => $action, 'page' => $page]);
$event = new Event(['action' => $action] + $options);
$this->container->fireEvent('onPageAction', $event);
$this->container->fireEvent('onPageAction.' . $action, $event);
}

View File

@@ -17,6 +17,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Config\ConfigFileFinder;
use Grav\Common\Config\Setup;
use Grav\Common\Language\Language;
use Grav\Framework\Mime\MimeTypes;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\File\YamlFile;
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
return $config;
};
$container['mime'] = function ($c) {
/** @var Config $config */
$config = $c['config'];
$mimes = $config->get('mime.types', []);
foreach ($config->get('media.types', []) as $ext => $media) {
if (!empty($media['mime'])) {
$mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
}
}
return MimeTypes::createFromMimes($mimes);
};
$container['languages'] = function ($c) {
return static::languages($c);
};

View File

@@ -1499,7 +1499,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
}
//Look for existing class
$svg = preg_replace_callback('/^<svg[^>]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) {
$svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
if (isset($matches[2])) {
$new_classes = $matches[2] . $classes;
$matched = true;

View File

@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
$compiler
->indent()
->subcompile($this->getNode('try'));
->subcompile($this->getNode('try'))
->outdent()
->write('} catch (\Exception $e) {' . "\n")
->indent()
->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
->write('$context[\'e\'] = $e;' . "\n");
if ($this->hasNode('catch')) {
$compiler
->outdent()
->write('} catch (\Exception $e) {' . "\n")
->indent()
->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
->write('$context[\'e\'] = $e;' . "\n")
->subcompile($this->getNode('catch'));
$compiler->subcompile($this->getNode('catch'));
}
$compiler

View File

@@ -239,9 +239,9 @@ class FlexDirectory implements FlexDirectoryInterface
// If configuration is found in main configuration, use it.
if (str_starts_with($uri, 'config://')) {
$path = strtr(substr($uri, 9, -5), '/', '.');
$path = str_replace('/', '.', substr($uri, 9, -5));
return $grav['config']->get($path);
return (array)$grav['config']->get($path);
}
// Load the configuration file.
@@ -831,7 +831,7 @@ class FlexDirectory implements FlexDirectoryInterface
* @param array $call
* @return void
*/
protected function dynamicFlexField(array &$field, $property, array $call)
protected function dynamicFlexField(array &$field, $property, array $call): void
{
$params = (array)$call['params'];
$object = $call['object'] ?? null;
@@ -840,11 +840,28 @@ class FlexDirectory implements FlexDirectoryInterface
if ($object && method_exists($object, $method)) {
$value = $object->{$method}(...$params);
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
$value = $this->mergeArrays($field[$property], $value);
}
$field[$property] = $value;
}
}
/**
* @param array $array1
* @param array $array2
* @return array
*/
protected function mergeArrays(array $array1, array $array2): array
{
foreach ($array2 as $key => $value) {
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
$array1[$key] = $this->mergeArrays($array1[$key], $value);
} else {
$field[$property] = $value;
$array1[$key] = $value;
}
}
return $array1;
}
/**

View File

@@ -453,7 +453,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'form' => $this->form,
'directory' => $this->directory,
'flexName' => $this->flexName
];
}
@@ -465,7 +467,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->form = $data['form'];
$this->directory = $data['directory'];
$this->flexName = $data['flexName'];
}
/**

View File

@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->name = $name;
$this->setObject($object);
$this->setName($object->getFlexType(), $name);
if (isset($options['form']['name'])) {
// Use custom form name.
$this->flexName = $options['form']['name'];
} else {
// Use standard form name.
$this->setName($object->getFlexType(), $name);
}
$this->setId($this->getName());
$uniqueId = $options['unique_id'] ?? null;
@@ -536,7 +543,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'items' => $this->items,
'form' => $this->form,
'object' => $this->object,
'flexName' => $this->flexName,
'submitMethod' => $this->submitMethod,
];
}
@@ -548,7 +559,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->object = $data['object'];
$this->items = $data['items'] ?? null;
$this->form = $data['form'] ?? null;
$this->object = $data['object'] ?? null;
$this->flexName = $data['flexName'] ?? null;
$this->submitMethod = $data['submitMethod'] ?? null;
}
/**

View File

@@ -44,6 +44,7 @@ use function is_array;
use function is_object;
use function is_scalar;
use function is_string;
use function json_encode;
/**
* Class FlexObject
@@ -820,11 +821,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function getForm(string $name = '', array $options = null)
{
if (!isset($this->_forms[$name])) {
$this->_forms[$name] = $this->createFormObject($name, $options);
$hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
if (!isset($this->_forms[$hash])) {
$this->_forms[$hash] = $this->createFormObject($name, $options);
}
return $this->_forms[$name];
return $this->_forms[$hash];
}
/**

View File

@@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage
protected $dataFolder;
/** @var string Pattern to access an object. */
protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
/** @var string[] */
protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
/** @var string Filename for the object. */
protected $dataFile;
/** @var string File extension for the object. */
@@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage
if (isset($data[0])) {
throw new RuntimeException('Broken object file');
}
// Add key field to the object.
$keyField = $this->keyField;
if ($keyField !== 'storage_key' && !isset($data[$keyField])) {
$data[$keyField] = $key;
}
} catch (RuntimeException $e) {
$data = ['__ERROR' => $e->getMessage()];
} finally {
@@ -692,9 +700,7 @@ class FolderStorage extends AbstractFilesystemStorage
$this->keyLen = (int)($options['key_len'] ?? 32);
$this->caseSensitive = (bool)($options['case_sensitive'] ?? true);
$variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
$pattern = Utils::simpleTemplate($pattern, $variables);
$pattern = Utils::simpleTemplate($pattern, $this->variables);
if (!$pattern) {
throw new RuntimeException('Bad storage folder pattern');
}

View File

@@ -120,7 +120,7 @@ trait FlexMediaTrait
// Load settings for the field.
$schema = $this->getBlueprint()->schema();
$settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
if (!isset($settings) || !is_array($settings)) {
if (!is_array($settings)) {
return null;
}

View File

@@ -0,0 +1,107 @@
<?php declare(strict_types=1);
/**
* @package Grav\Framework\Mime
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Mime;
use function in_array;
/**
* Class to handle mime-types.
*/
class MimeTypes
{
/** @var array */
protected $extensions;
/** @var array */
protected $mimes;
/**
* Create a new mime types instance with the given mappings.
*
* @param array $mimes An associative array containing ['ext' => ['mime/type', 'mime/type2']]
*/
public static function createFromMimes(array $mimes): self
{
$extensions = [];
foreach ($mimes as $ext => $list) {
foreach ($list as $mime) {
$list = $extensions[$mime] ?? [];
if (!in_array($ext, $list, true)) {
$list[] = $ext;
$extensions[$mime] = $list;
}
}
}
return new static($extensions, $mimes);
}
/**
* @param string $extension
* @return string|null
*/
public function getMimeType(string $extension): ?string
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension][0] ?? null;
}
/**
* @param string $mime
* @return string|null
*/
public function getExtension(string $mime): ?string
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime][0] ?? null;
}
/**
* @param string $extension
* @return array
*/
public function getMimeTypes(string $extension): array
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension] ?? [];
}
/**
* @param string $mime
* @return array
*/
public function getExtensions(string $mime): array
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime] ?? [];
}
/**
* @param string $input
* @return string
*/
protected function cleanInput(string $input): string
{
return strtolower(trim($input));
}
/**
* @param array $extensions
* @param array $mimes
*/
protected function __construct(array $extensions, array $mimes)
{
$this->extensions = $extensions;
$this->mimes = $mimes;
}
}

View File

@@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface
{
use UploadedFileDecoratorTrait;
/** @var array */
private $meta = [];
/**
* @param StreamInterface|string|resource $streamOrFile
* @param int $size
@@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface
{
$this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType);
}
/**
* @param array $meta
* @return $this
*/
public function setMeta(array $meta)
{
$this->meta = $meta;
return $this;
}
/**
* @param array $meta
* @return $this
*/
public function addMeta(array $meta)
{
$this->meta = array_merge($this->meta, $meta);
return $this;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
}