This commit is contained in:
Ouidade Soussi Chiadmi 2021-09-16 14:44:40 +02:00
parent 8bd1b83c5f
commit 4ca5c9f82d
55 changed files with 3279 additions and 482 deletions

View File

@ -1,3 +1,83 @@
# v1.7.21
## 09/14/2021
1. [](#new)
* Added `|yaml` filter to convert input to YAML
* Added `route` and `request` to `onPageNotFound` event
* Added file upload/remove support for `Flex Forms`
* Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints
* Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()`
* Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages
2. [](#improved)
* Use a simplified text-based `cron` field for scheduler
* Add timestamp to logging output of scheduler jobs to see when they ran
3. [](#bugfix)
* Fixed escaping in PageIndex::getLevelListing()
* Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433)
* Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432)
* Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439)
* Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie
# v1.7.20
## 09/01/2021
2. [](#improved)
* Added support for `task` and `action` inside JSON request body
# 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
1. [](#improved)
* Added support for loading Flex Directory configuration from main configuration
* Move SVGs that cannot be sanitized to quarantine folder under `log://quarantine`
* Added support for CloudFlare-forwarded client IP in the `URI::ip()` method
1. [](#bugfix)
* Fixed error when using Flex `SimpleStorage` with no entries
* Fixed page search to include slug field [#3316](https://github.com/getgrav/grav/issues/3316)
* Fixed Admin becoming unusable when GPM cannot be reached [#3383](https://github.com/getgrav/grav/issues/3383)
* Fixed `Failed to save entry: Forbidden` when moving a page to a visible page [#3389](https://github.com/getgrav/grav/issues/3389)
* Better support for Symfony local server on linux [#3400](https://github.com/getgrav/grav/pull/3400)
* Fixed `open_basedir()` error with some forms
# v1.7.17
## 06/15/2021
1. [](#new)
* Interface `FlexDirectoryInterface` now extends `FlexAuthorizeInterface`
1. [](#improved)
* Allow to unset an asset attribute by specifying null (ie, `'defer': null`)
* Support specifying custom attributes to assets in a collection [Read more](https://learn.getgrav.org/17/themes/asset-manager#collections-with-attributes?target=_blank) [#3358](https://github.com/getgrav/grav/issues/3358)
* File `frontmatter.yaml` isn't part of media, ignore it
* Switched default `JQuery` collection to use 3.x rather than 2.x
1. [](#bugfix)
* Fixed missing styles when CSS/JS Pipeline is used and `asset://` folder is missing
* Fixed permission check when moving a page [#3382](https://github.com/getgrav/grav/issues/3382)
# v1.7.16
## 06/02/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)

607
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ 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;
$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

@ -47,7 +47,8 @@ form:
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
placeholder: '-lah'
.at:
type: cron
type: text
wrapper_classes: cron-selector
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
placeholder: '* * * * *'

View File

@ -184,9 +184,9 @@ config:
# Fields to be searched
fields:
- key
- slug
- menu
- title
- name
blueprints:
configure:

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

@ -96,7 +96,7 @@ cache:
purge_at: '0 4 * * *' # How often to purge old file cache (using new scheduler)
clear_at: '0 3 * * *' # How often to clear cache (using new scheduler)
clear_job_type: 'standard' # Type to clear when processing the scheduled clear job `standard`|`all`
clear_images_by_default: false # By default grav will include processed images in cache clear, this can be disabled
clear_images_by_default: false # By default grav does not include processed images in cache clear, this can be enabled
cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
gzip: false # GZip compress the page output
@ -131,7 +131,7 @@ assets: # Configuration for Assets Mana
enable_asset_timestamp: false # Enable asset timestamps
enable_asset_sri: false # Enable asset SRI
collections:
jquery: system://assets/jquery/jquery-2.x.min.js
jquery: system://assets/jquery/jquery-3.x.min.js
errors:
display: 0 # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error

View File

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

View File

@ -110,7 +110,7 @@ class Assets extends PropertyObject
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_dir = $locator->findResource('asset://');
$this->assets_url = $locator->findResource('asset://', false);
$this->config($asset_config);
@ -164,10 +164,19 @@ class Assets extends PropertyObject
// More than one asset
if (is_array($asset)) {
foreach ($asset as $a) {
array_shift($args);
$args = array_merge([$a], $args);
call_user_func_array([$this, 'add'], $args);
foreach ($asset as $index => $location) {
$params = array_slice($args, 1);
if (is_array($location)) {
$params = array_shift($params);
if (is_numeric($params)) {
$params = [ 'priority' => $params ];
}
$params = [array_replace_recursive([], $location, $params)];
$location = $index;
}
$params = array_merge([$location], $params);
call_user_func_array([$this, 'add'], $params);
}
} elseif (isset($this->collections[$asset])) {
array_shift($args);
@ -201,8 +210,13 @@ class Assets extends PropertyObject
protected function addType($collection, $type, $asset, $options)
{
if (is_array($asset)) {
foreach ($asset as $a) {
$this->addType($collection, $type, $a, $options);
foreach ($asset as $index => $location) {
$assetOptions = $options;
if (is_array($location)) {
$assetOptions = array_replace_recursive([], $options, $location);
$location = $index;
}
$this->addType($collection, $type, $location, $assetOptions);
}
return $this;

View File

@ -9,9 +9,9 @@
namespace Grav\Common\Assets;
use Grav\Common\Assets\BaseAsset;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
$uri = Grav::instance()['uri'];
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_dir = $locator->findResource('asset://');
if (!$this->assets_dir) {
// Attempt to create assets folder if it doesn't exist yet.
$this->assets_dir = $locator->findResource('asset://', true, true);
Folder::mkdir($this->assets_dir);
$locator->clearCache();
}
$this->assets_url = $locator->findResource('asset://', false);
}
@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.css';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$buffer = null;
if (file_exists($this->assets_dir . $file)) {
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
$filepath = "{$this->assets_dir}/{$file}";
if (file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
// Write file
if (trim($buffer) !== '') {
file_put_contents($this->assets_dir . $file, $buffer);
file_put_contents($filepath, $buffer);
}
}
@ -182,10 +188,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$buffer = null;
if (file_exists($this->assets_dir . $file)) {
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
$filepath = "{$this->assets_dir}/{$file}";
if (file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
// Write file
if (trim($buffer) !== '') {
file_put_contents($this->assets_dir . $file, $buffer);
file_put_contents($filepath, $buffer);
}
}

View File

@ -156,6 +156,10 @@ trait AssetUtilsTrait
$no_key = ['loading'];
foreach ($this->attributes as $key => $value) {
if ($value === null) {
continue;
}
if (is_numeric($key)) {
$key = $value;
}

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,19 @@ 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);
// Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
$securityFile = basename(static::$securityFile);
$securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
$securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
$filename = "{$securityFolder}/{$securityFile}";
$security_file = CompiledYamlFile::instance($filename);
$security_content = (array)$security_file->content();

View File

@ -519,17 +519,32 @@ 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'];
// Count of how many steps we are above/below the minimum value.
$pos = ($value - $min) / $step;
return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
return is_int(static::filterNumber($pos, $params, $field));
}
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

@ -674,12 +674,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$count = $filters ? $tmp->filterBy($filters, true)->count() : null;
$route = $child->getRoute();
$payload = [
'item-key' => basename($child->rawRoute() ?? $child->getKey()),
'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
'icon' => $icon,
'title' => htmlspecialchars($child->menu()),
'route' => [
'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
'raw' => $child->rawRoute(),
'display' => htmlspecialchars(($route ? ($route->toString(false) ?: '/') : null) ?? ''),
'raw' => htmlspecialchars($child->rawRoute()),
],
'modified' => $this->jsDate($child->modified()),
'child_count' => $child_count ?: null,

View File

@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
$this->getFlexDirectory()->reloadIndex();
}
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
{
parent::check($user);
if ($user && $this->isMoved()) {
$parentKey = $this->getProperty('parent_key');
/** @var PageObject|null $parent */
$parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
if (!$parent || !$parent->isAuthorized('create', null, $user)) {
throw new \RuntimeException('Forbidden', 403);
}
}
}
/**
* @param array|bool $reorder
* @return FlexObject|FlexObjectInterface
@ -357,6 +375,19 @@ class PageObject extends FlexPageObject
return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
}
/**
* @return bool
*/
protected function isMoved(): bool
{
$storageKey = $this->getMasterKey();
$filesystem = Filesystem::getInstance(false);
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
$newParentKey = $this->getProperty('parent_key');
return $this->exists() && $oldParentKey !== $newParentKey;
}
/**
* @param array $ordering
* @return PageCollection|null
@ -364,10 +395,7 @@ class PageObject extends FlexPageObject
protected function reorderSiblings(array $ordering)
{
$storageKey = $this->getMasterKey();
$filesystem = Filesystem::getInstance(false);
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
$newParentKey = $this->getProperty('parent_key');
$isMoved = $this->exists() && $oldParentKey !== $newParentKey;
$isMoved = $this->isMoved();
$order = !$isMoved ? $this->order() : false;
if ($order !== false) {
$order = (int)$order;

View File

@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
] + parent::getCachedMethods();
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->getProperty('readableName');
}
/**
* Checks user authorization to the action.
*

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;
@ -31,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Storage\FileStorage;
@ -75,6 +77,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 +264,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);
@ -292,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $value;
}
/**
* @return UserGroupIndex
*/
public function getRoles(): UserGroupIndex
{
return $this->getGroups();
}
/**
* Convert object into an array.
*
@ -689,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
/**
* @param array $files
* @return void
*/
protected function setUpdatedMedia(array $files): void
{
@ -700,9 +723,12 @@ class UserObject extends FlexObject implements UserInterface, Countable
return;
}
$filesystem = Filesystem::getInstance(false);
$list = [];
$list_original = [];
foreach ($files as $field => $group) {
// Ignore files without a field.
if ($field === '') {
continue;
}
@ -724,7 +750,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
if ($file) {
// Check file upload against media limits.
// Check file upload against media limits (except for max size).
$filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
}
@ -748,15 +774,19 @@ class UserObject extends FlexObject implements UserInterface, Countable
continue;
}
// Calculate path without the retina scaling factor.
$realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
$list[$filename] = [$file, $settings];
$path = str_replace('.', "\n", $field);
if (null !== $data) {
$data['name'] = $filename;
$data['path'] = $filepath;
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
$this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
} else {
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
$this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
}
}
}

View File

@ -35,7 +35,11 @@ class GPM extends Iterator
/** @var Remote\Packages|null Remote available Packages */
private $repository;
/** @var Remote\GravCore|null Remove Grav Packages */
public $grav;
private $grav;
/** @var bool */
private $refresh;
/** @var callable|null */
private $callback;
/** @var array Internal cache */
protected $cache;
@ -55,13 +59,45 @@ class GPM extends Iterator
public function __construct($refresh = false, $callback = null)
{
parent::__construct();
Folder::create(CACHE_DIR . '/gpm');
$this->cache = [];
$this->installed = new Local\Packages();
try {
$this->repository = new Remote\Packages($refresh, $callback);
$this->grav = new Remote\GravCore($refresh, $callback);
} catch (Exception $e) {
$this->refresh = $refresh;
$this->callback = $callback;
}
/**
* Magic getter method
*
* @param string $offset Asset name value
* @return mixed Asset value
*/
public function __get($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav();
}
return parent::__get($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param string $offset Asset name value
* @return bool True if the value is set
*/
public function __isset($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav() !== null;
}
return parent::__isset($offset);
}
/**
@ -266,11 +302,12 @@ class GPM extends Iterator
{
$items = [];
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return $items;
}
$repository = $this->repository['plugins'];
$plugins = $repository['plugins'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@ -278,18 +315,18 @@ class GPM extends Iterator
}
foreach ($this->installed['plugins'] as $slug => $plugin) {
if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $repository[$slug]->version;
$remote_version = $plugins[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
$repository[$slug]->available = $remote_version;
$repository[$slug]->version = $local_version;
$repository[$slug]->type = $repository[$slug]->release_type;
$items[$slug] = $repository[$slug];
$plugins[$slug]->available = $remote_version;
$plugins[$slug]->version = $local_version;
$plugins[$slug]->type = $plugins[$slug]->release_type;
$items[$slug] = $plugins[$slug];
}
}
@ -306,19 +343,20 @@ class GPM extends Iterator
*/
public function getLatestVersionOfPackage($package_name)
{
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return null;
}
$repository = $this->repository['plugins'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->available ?: $repository[$package_name]->version;
$plugins = $repository['plugins'];
if (isset($plugins[$package_name])) {
return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
}
//Not a plugin, it's a theme?
$repository = $this->repository['themes'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->available ?: $repository[$package_name]->version;
$themes = $repository['themes'];
if (isset($themes[$package_name])) {
return $themes[$package_name]->available ?: $themes[$package_name]->version;
}
return null;
@ -356,11 +394,12 @@ class GPM extends Iterator
{
$items = [];
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return $items;
}
$repository = $this->repository['themes'];
$themes = $repository['themes'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@ -368,18 +407,18 @@ class GPM extends Iterator
}
foreach ($this->installed['themes'] as $slug => $plugin) {
if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $repository[$slug]->version;
$remote_version = $themes[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
$repository[$slug]->available = $remote_version;
$repository[$slug]->version = $local_version;
$repository[$slug]->type = $repository[$slug]->release_type;
$items[$slug] = $repository[$slug];
$themes[$slug]->available = $remote_version;
$themes[$slug]->version = $local_version;
$themes[$slug]->type = $themes[$slug]->release_type;
$items[$slug] = $themes[$slug];
}
}
@ -407,19 +446,20 @@ class GPM extends Iterator
*/
public function getReleaseType($package_name)
{
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return null;
}
$repository = $this->repository['plugins'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->release_type;
$plugins = $repository['plugins'];
if (isset($plugins[$package_name])) {
return $plugins[$package_name]->release_type;
}
//Not a plugin, it's a theme?
$repository = $this->repository['themes'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->release_type;
$themes = $repository['themes'];
if (isset($themes[$package_name])) {
return $themes[$package_name]->release_type;
}
return null;
@ -470,7 +510,7 @@ class GPM extends Iterator
*/
public function getRepositoryPlugins()
{
return $this->repository['plugins'] ?? null;
return $this->getRepository()['plugins'] ?? null;
}
/**
@ -493,7 +533,7 @@ class GPM extends Iterator
*/
public function getRepositoryThemes()
{
return $this->repository['themes'] ?? null;
return $this->getRepository()['themes'] ?? null;
}
/**
@ -504,9 +544,31 @@ class GPM extends Iterator
*/
public function getRepository()
{
if (null === $this->repository) {
try {
$this->repository = new Remote\Packages($this->refresh, $this->callback);
} catch (Exception $e) {}
}
return $this->repository;
}
/**
* Returns Grav version available in the repository
*
* @return Remote\GravCore|null
*/
public function getGrav()
{
if (null === $this->grav) {
try {
$this->grav = new Remote\GravCore($this->refresh, $this->callback);
} catch (Exception $e) {}
}
return $this->grav;
}
/**
* Searches for a Package in the repository
*

View File

@ -9,6 +9,7 @@
namespace Grav\Common;
use Composer\Autoload\ClassLoader;
use Grav\Common\Config\Config;
use Grav\Common\Config\Setup;
use Grav\Common\Helpers\Exif;
@ -152,6 +153,13 @@ class Grav extends Container
{
if (null === self::$instance) {
self::$instance = static::load($values);
/** @var ClassLoader|null $loader */
$loader = self::$instance['loader'] ?? null;
if ($loader) {
// Load fix for Deferred Twig Extension
$loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
}
} elseif ($values) {
$instance = self::$instance;
foreach ($values as $key => $value) {

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

@ -102,12 +102,13 @@ class Media extends AbstractMedia
foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
$filename = $info->getFilename();
if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
continue;
}
// Find out what type we're dealing with
[$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
[$basename, $ext, $type, $extra] = $this->getFileParts($filename);
if (!in_array(strtolower($ext), $media_types, true)) {
continue;

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,8 @@
namespace Grav\Common\Processors;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Plugin\Form\Forms;
use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
$page = $this->container['page'];
if (!$page->routable()) {
$exception = new RequestException($request, 'Page Not Found', 404);
$route = $this->container['route'];
// If no page found, fire event
$event = new Event(['page' => $page]);
$event = new Event([
'page' => $page,
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'exception' => $exception,
'route' => $route,
'request' => $request
]);
$event->page = null;
$event = $this->container->fireEvent('onPageNotFound', $event);
@ -65,12 +76,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

@ -390,7 +390,9 @@ class Job
if (count($this->outputTo) > 0) {
foreach ($this->outputTo as $file) {
$output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
file_put_contents($file, $this->output, $output_mode);
$timestamp = (new DateTime('now'))->format('c');
$output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
file_put_contents($file, $output, $output_mode);
}
}

View File

@ -12,6 +12,7 @@ namespace Grav\Common;
use enshrined\svgSanitize\Sanitizer;
use Exception;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Page\Pages;
use function chr;
use function count;
@ -56,9 +57,16 @@ class Security
$original_svg = file_get_contents($file);
$clean_svg = $sanitizer->sanitize($original_svg);
// TODO: what to do with bad SVG files which return false?
if ($clean_svg !== false && $clean_svg !== $original_svg) {
// Quarantine bad SVG files and throw exception
if ($clean_svg !== false ) {
file_put_contents($file, $clean_svg);
} else {
$quarantine_file = basename($file);
$quarantine_dir = 'log://quarantine';
Folder::mkdir($quarantine_dir);
file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
unlink($file);
throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
}
}
}

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

@ -12,6 +12,7 @@ namespace Grav\Common\Service;
use Grav\Common\Grav;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Class TaskServiceProvider
@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
public function register(Container $container)
{
$container['task'] = function (Grav $c) {
$task = $_POST['task'] ?? $c['uri']->param('task');
/** @var ServerRequestInterface $request */
$request = $c['request'];
$body = $request->getParsedBody();
$task = $body['task'] ?? $c['uri']->param('task');
if (null !== $task) {
$task = filter_var($task, FILTER_SANITIZE_STRING);
}
@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
};
$container['action'] = function (Grav $c) {
$action = $_POST['action'] ?? $c['uri']->param('action');
/** @var ServerRequestInterface $request */
$request = $c['request'];
$body = $request->getParsedBody();
$action = $body['action'] ?? $c['uri']->param('action');
if (null !== $action) {
$action = filter_var($action, FILTER_SANITIZE_STRING);
}

View File

@ -12,6 +12,7 @@ namespace Grav\Common;
use Grav\Common\Form\FormFlash;
use Grav\Events\SessionStartEvent;
use Grav\Plugin\Form\Forms;
use JsonException;
use function is_string;
/**
@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
* @param mixed $object
* @param int $time
* @return $this
* @throws JsonException
*/
public function setFlashCookieObject($name, $object, $time = 60)
{
setcookie($name, json_encode($object), time() + $time, '/');
setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
return $this;
}
@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
*
* @param string $name
* @return mixed|null
* @throws JsonException
*/
public function getFlashCookieObject($name)
{
if (isset($_COOKIE[$name])) {
$object = json_decode($_COOKIE[$name], false);
setcookie($name, '', time() - 3600, '/');
return $object;
$cookie = $_COOKIE[$name];
setcookie($name, '', $this->getCookieOptions(-42000));
return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
}
return null;

View File

@ -0,0 +1,19 @@
<?php
/**
* @package Grav\Common\Twig\Exception
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\Exception;
/**
* TwigException gets thrown when you use {% throw code message %} in twig.
*
* This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
*/
class TwigException extends \RuntimeException
{
}

View File

@ -155,6 +155,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
new TwigFilter('bool', [$this, 'boolFilter']),
new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
new TwigFilter('array', [$this, 'arrayFilter']),
new TwigFilter('yaml', [$this, 'yamlFilter']),
// Object Types
new TwigFilter('get_type', [$this, 'getTypeFunc']),
@ -807,6 +808,17 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
return (array)$input;
}
/**
* @param array|object $value
* @param int|null $inline
* @param int|null $indent
* @return string
*/
public function yamlFilter($value, $inline = null, $indent = null): string
{
return Yaml::dump($value, $inline, $indent);
}
/**
* @param Environment $twig
* @return string
@ -1499,7 +1511,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

@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
$compiler->addDebugInfo($this);
$compiler
->write('throw new \RuntimeException(')
->write('throw new \Grav\Common\Twig\Exception\TwigException(')
->subcompile($this->getNode('message'))
->write(', ')
->write($this->getAttribute('code') ?: 500)

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

@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
use Grav\Common\Language\LanguageCodes;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Common\Twig\Exception\TwigException;
use Grav\Common\Twig\Extension\FilesystemExtension;
use Grav\Common\Twig\Extension\GravExtension;
use Grav\Common\Utils;
@ -26,6 +27,7 @@ use RuntimeException;
use Twig\Cache\FilesystemCache;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\StringLoaderExtension;
@ -404,38 +406,63 @@ class Twig
*/
public function processSite($format = null, array $vars = [])
{
// set the page now its been processed
$this->grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $this->grav['pages'];
/** @var PageInterface $page */
$page = $this->grav['page'];
$content = $page->content();
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $this->grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $content;
// determine if params are set, if so disable twig cache
$params = $this->grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
try {
$grav = $this->grav;
// set the page now its been processed
$grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $grav['pages'];
/** @var PageInterface $page */
$page = $grav['page'];
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $page->content();
// determine if params are set, if so disable twig cache
$params = $grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
$output = $this->twig->render($template, $vars + $twig_vars);
} catch (LoaderError $e) {
$error_msg = $e->getMessage();
throw new RuntimeException($error_msg, 400, $e);
throw new RuntimeException($e->getMessage(), 400, $e);
} catch (RuntimeError $e) {
$prev = $e->getPrevious();
if ($prev instanceof TwigException) {
$code = $prev->getCode() ?: 500;
// Fire onPageNotFound event.
$event = new Event([
'page' => $page,
'code' => $code,
'message' => $prev->getMessage(),
'exception' => $prev,
'route' => $grav['route'],
'request' => $grav['request']
]);
$event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
$newPage = $event['page'];
if ($newPage && $newPage !== $page) {
unset($grav['page']);
$grav['page'] = $newPage;
return $this->processSite($newPage->templateFormat(), $vars);
}
}
throw $e;
}
return $output;

View File

@ -675,10 +675,15 @@ class Uri
*/
public static function ip()
{
$ip = 'UNKNOWN';
if (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif (getenv('HTTP_CF_CONNECTING_IP')) {
$ip = getenv('HTTP_CF_CONNECTING_IP');
} elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
$ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR')));
$ip = array_shift($ips);
} elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED');
} elseif (getenv('HTTP_FORWARDED_FOR')) {
@ -687,8 +692,6 @@ class Uri
$ip = getenv('HTTP_FORWARDED');
} elseif (getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
} else {
$ip = 'UNKNOWN';
}
return $ip;
@ -1258,7 +1261,7 @@ class Uri
$this->port = null;
}
if ($this->hasStandardPort()) {
if ($this->port === 0 || $this->hasStandardPort()) {
$this->port = null;
}
@ -1311,11 +1314,13 @@ class Uri
if ($parts === false) {
throw new RuntimeException('Malformed URL: ' . $url);
}
$port = (int)($parts['port'] ?? 0);
$this->scheme = $parts['scheme'] ?? null;
$this->user = $parts['user'] ?? null;
$this->password = $parts['pass'] ?? null;
$this->host = $parts['host'] ?? null;
$this->port = isset($parts['port']) ? (int)$parts['port'] : null;
$this->port = $port ?: null;
$this->path = $parts['path'] ?? '';
$this->query = $parts['query'] ?? '';
$this->fragment = $parts['fragment'] ?? null;

View File

@ -47,7 +47,7 @@ use function is_callable;
* @package Grav\Framework\Flex
* @template T
*/
class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
class FlexDirectory implements FlexDirectoryInterface
{
use FlexAuthorizeTrait;
@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
$uri = $this->getDirectoryConfigUri($name);
// If configuration is found in main configuration, use it.
if (str_starts_with($uri, 'config://')) {
$path = str_replace('/', '.', substr($uri, 9, -5));
return (array)$grav['config']->get($path);
}
// Load the configuration file.
$filename = $locator->findResource($uri, true);
if ($filename === false) {
return [];
}
@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
* @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;
$method = array_shift($params);
$not = false;
if (str_starts_with($method, '!')) {
$method = substr($method, 1);
$not = true;
} elseif (str_starts_with($method, 'not ')) {
$method = substr($method, 4);
$not = true;
}
$method = trim($method);
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] = $not ? !$value : $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

@ -318,11 +318,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
return null;
}
@ -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;
@ -371,22 +378,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.upload');
}
return $object->route('/edit.json/task:media.upload');
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.delete');
}
return $object->route('/edit.json/task:media.delete');
@ -536,7 +549,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 +565,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
@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/** @var array */
private $_meta;
/** @var array */
protected $_original;
/** @var array */
protected $_changes;
/** @var string */
protected $storage_key;
@ -369,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
$value = $this->getProperty($property);
return $this->searchValue($property, $value, $search, $options);
@ -383,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchNestedProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
if ($property === 'key') {
$value = $this->getKey();
} else {
@ -440,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return 0;
}
/**
* Get original data before update
*
* @return array
*/
public function getOriginalData(): array
{
return $this->_original ?? [];
}
/**
* Get any changes based on data sent to update
*
@ -653,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
// Store the changes
$this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
$this->_original = $this->getElements();
$this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
}
if ($files && method_exists($this, 'setUpdatedMedia')) {
@ -691,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $this->create($key);
}
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
{
// If user has been provided, check if the user has permissions to save this object.
if ($user && !$this->isAuthorized('save', null, $user)) {
throw new \RuntimeException('Forbidden', 403);
}
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::save()
@ -809,11 +834,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];
}
/**
@ -1063,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $action;
}
/**
* Method to reset blueprints if the type changes.
*
* @return void
* @since 1.7.18
*/
protected function resetBlueprints(): void
{
$this->_blueprint = [];
}
// DEPRECATED METHODS
/**

View File

@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
* Interface FlexDirectoryInterface
* @package Grav\Framework\Flex\Interfaces
*/
interface FlexDirectoryInterface
interface FlexDirectoryInterface extends FlexAuthorizeInterface
{
/**
* @return bool

View File

@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
/**
* Get route for deleting files by AJAX.
*
* @param string $field Field where the file is associated into.
* @param string $filename Filename for the file.
* @param string|null $field Field where the file is associated into.
* @param string|null $filename Filename for the file.
* @return Route|null Returns Route object or null if file uploads are not enabled.
*/
public function getFileDeleteAjaxRoute($field, $filename);

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

@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage
$content = (array) $file->content();
if ($this->prefix) {
$data = new Data($content);
$content = $data->get($this->prefix);
$content = $data->get($this->prefix, []);
}
$file->free();

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

@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface
protected function loadStoredForm(): ?array
{
$file = $this->getTmpIndex();
$exists = $file->exists();
$exists = $file && $file->exists();
$data = null;
if ($exists) {
@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface
if ($force || $this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
if ($file) {
$file->save($this->jsonSerialize());
$this->exists = true;
}
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface
}
/**
* @return YamlFile
* @return ?YamlFile
*/
protected function getTmpIndex(): YamlFile
protected function getTmpIndex(): ?YamlFile
{
$tmpDir = $this->getTmpDir();
// Do not use CompiledYamlFile as the file can change multiple times per second.
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
}
/**
@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface
{
// Make sure that index file cache gets always cleared.
$file = $this->getTmpIndex();
$file->free();
if ($file) {
$file->free();
}
$tmpDir = $this->getTmpDir();
if ($tmpDir && file_exists($tmpDir)) {

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

@ -9,7 +9,6 @@
namespace Grav\Framework\Object;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Framework\Collection\ArrayCollection;
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;

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;
}
}

View File

@ -338,23 +338,12 @@ class Session implements SessionInterface
{
$name = $this->getName();
if (null !== $name) {
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
$this->removeCookie();
setcookie(
session_name(),
'',
$cookie_options
$this->getCookieOptions(-42000)
);
}
@ -463,27 +452,36 @@ class Session implements SessionInterface
}
/**
* @return void
* Store something in cookie temporarily.
*
* @param int|null $lifetime
* @return array
*/
protected function setCookie(): void
public function getCookieOptions(int $lifetime = null): array
{
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() + $params['lifetime'],
return [
'expires' => time() + ($lifetime ?? $params['lifetime']),
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
];
}
/**
* @return void
*/
protected function setCookie(): void
{
$this->removeCookie();
setcookie(
session_name(),
session_id(),
$cookie_options
$this->getCookieOptions()
);
}

View File

@ -0,0 +1,70 @@
<?php
// Fix too many ob_get_clean() calls when exception is thrown inside the template.
namespace Phive\Twig\Extensions\Deferred;
class DeferredExtension extends \Twig_Extension
{
/**
* @var array
*/
private $blocks = array();
/**
* {@inheritdoc}
*/
public function getTokenParsers()
{
return array(new DeferredTokenParser());
}
/**
* {@inheritdoc}
*/
public function getNodeVisitors()
{
return array(new DeferredNodeVisitor());
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'deferred';
}
public function defer(\Twig_Template $template, $blockName)
{
ob_start();
$templateName = $template->getTemplateName();
$this->blocks[$templateName][] = [ob_get_level(), $blockName];
}
public function resolve(\Twig_Template $template, array $context, array $blocks)
{
$templateName = $template->getTemplateName();
if (empty($this->blocks[$templateName])) {
return;
}
while ($block = array_pop($this->blocks[$templateName])) {
[$level, $blockName] = $block;
if (ob_get_level() !== $level) {
continue;
}
$buffer = ob_get_clean();
$blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred');
$template->displayBlock($blockName, $context, $blocks);
echo $buffer;
}
if ($parent = $template->getParent($context)) {
$this->resolve($parent, $context, $blocks);
}
}
}

View File

@ -1,6 +1,7 @@
core:
grav:
version: 1.7.16
version: 1.7.21
schema: 1.7.0_2020-11-20_1
history:
- { version: 1.7.16, date: '2021-06-10 14:03:35' }
- { version: 1.7.21, date: '2021-09-16 12:41:14' }