This commit is contained in:
2022-10-18 10:29:26 +02:00
parent 5d2f6b6dd0
commit eeefd826e5
70 changed files with 3175 additions and 815 deletions
+78
View File
@@ -1,3 +1,81 @@
# v1.7.37.1
## 10/05/2022
1. [](#bugfix)
* Fixed a bad return type [#3630](https://github.com/getgrav/grav/issues/3630)
# v1.7.37
## 10/05/2022
1. [](#new)
* Added new `onPageHeaders()` event to allow for header modification as needed
* Added a `system.pages.dirs` configuration option to allow for configurable paths, and multiple page paths
* Added new `Pages::getSimplePagesHash` which is useful for caching pages specific data
* Updated to latest vendor libraries
1. [](#bugfix)
* An attempt to workaround windows reading locked file issue [getgrav/grav-plugin-admin#2299](https://github.com/getgrav/grav-plugin-admin/issues/2299)
* Force user index file to be updated to fix email addresses [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229)
# v1.7.36
## 09/08/2022
1. [](#new)
* Added `authorize-*@:` support for Flex blueprints, e.g. `authorize-disabled@: not delete` disables the field if user does not have access to delete object
* Added support for `flex-ignore@` to hide all the nested fields in the blueprint
1. [](#bugfix)
* Fixed login with a capitalised email address when using old users [getgrav/grav-plugin-login#229](https://github.com/getgrav/grav-plugin-login/issues/229)
# v1.7.35
## 08/04/2022
1. [](#new)
* Added support for `multipart/form-data` content type in PUT and PATCH requests
* Added support for object relationships
* Added variables `$environment` (string), `$request` (PSR-7 ServerRequestInterface|null) and `$uri` (PSR-7 Uri|null) to be used in `setup.php`
1. [](#improved)
* Minor vendor updates
# v1.7.34
## 06/14/2022
1. [](#new)
* Added back Yiddish to Language Codes [#3336](https://github.com/getgrav/grav/pull/3336)
* Ignore upcoming `media.json` file in media
1. [](#bugfix)
* Regression: Fixed saving page with a new language causing cache corruption [getgrav/grav-plugin-admin#2282](https://github.com/getgrav/grav-plugin-admin/issues/2282)
* Fixed a potential fatal error when using watermark in images
* Fixed `bin/grav install` command with arbitrary destination folder name
* Fixed Twig `|filter()` allowing code execution
* Fixed login and user search by email not being case-insensitive when using Flex Users
# v1.7.33
## 04/25/2022
1. [](#improved)
* When saving yaml and markdown, create also a cached version of the file and recompile it in opcache
2. [](#bugfix)
* Fixed missing changes in **yaml** & **markdown** files if saved multiple times during the same second because of a caching issue
* Fixed XSS check not detecting onX events without quotes
* Fixed default collection ordering in pages admin
# v1.7.32
## 03/28/2022
1. [](#new)
* Added `|replace_last(search, replace)` filter
* Added `parseurl` Twig function to expose PHP's `parse_url` function
2. [](#improved)
* Added multi-language support for page routes in `Utils::url()`
* Set default maximum length for text fields
- `password`: 256
- `email`: 320
- `text`, `url`, `hidden`, `commalist`: 2048
- `text` (multiline), `textarea`: 65536
3. [](#bugfix)
* Fixed issue with `system.cache.gzip: true` resulted in "Fetch Failed" for PHP 8.0.17 and PHP 8.1.4 [PHP issue #8218](https://github.com/php/php-src/issues/8218)
* Fix for multi-lang issues with Security Report
* Fixed page search not working with selected language [#3316](https://github.com/getgrav/grav/issues/3316)
# v1.7.31 # v1.7.31
## 03/14/2022 ## 03/14/2022
+2 -1
View File
@@ -19,6 +19,7 @@
"ext-zip": "*", "ext-zip": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-gd": "*",
"symfony/polyfill-mbstring": "~1.23", "symfony/polyfill-mbstring": "~1.23",
"symfony/polyfill-iconv": "^1.23", "symfony/polyfill-iconv": "^1.23",
"symfony/polyfill-php74": "^1.23", "symfony/polyfill-php74": "^1.23",
@@ -64,7 +65,7 @@
}, },
"require-dev": { "require-dev": {
"codeception/codeception": "^4.1", "codeception/codeception": "^4.1",
"phpstan/phpstan": "^1.2", "phpstan/phpstan": "^1.8",
"phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0",
"phpunit/php-code-coverage": "~9.2", "phpunit/php-code-coverage": "~9.2",
"getgrav/markdowndocs": "^2.0", "getgrav/markdowndocs": "^2.0",
Generated
+362 -611
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1156,6 +1156,13 @@ form:
local6: local6 local6: local6
local7: local7 local7: local7
log.syslog.tag:
type: text
size: small
label: PLUGIN_ADMIN.SYSLOG_TAG
help: PLUGIN_ADMIN.SYSLOG_TAG_HELP
placeholder: "grav"
debugger: debugger:
type: tab type: tab
title: PLUGIN_ADMIN.DEBUGGER title: PLUGIN_ADMIN.DEBUGGER
+11
View File
@@ -125,6 +125,17 @@ config:
- username - username
- fullname - fullname
relationships:
media:
type: media
cardinality: to-many
avatar:
type: media
cardinality: to-one
# roles:
# type: user-groups
# cardinality: to-many
blueprints: blueprints:
configure: configure:
fields: fields:
+2
View File
@@ -35,6 +35,7 @@ home:
pages: pages:
type: regular # EXPERIMENTAL: Page type: regular or flex type: regular # EXPERIMENTAL: Page type: regular or flex
dirs: ['page://'] # Advanced functionality, allows for multiple page paths
theme: quark # Default theme (defaults to "quark" theme) theme: quark # Default theme (defaults to "quark" theme)
order: order:
by: default # Order pages by "default", "alpha" or "date" by: default # Order pages by "default", "alpha" or "date"
@@ -144,6 +145,7 @@ log:
handler: file # Log handler. Currently supported: file | syslog handler: file # Log handler. Currently supported: file | syslog
syslog: syslog:
facility: local6 # Syslog facilities output facility: local6 # Syslog facilities output
tag: grav # Syslog tag. Default: "grav".
debugger: debugger:
enabled: false # Enable Grav debugger and following settings enabled: false # Enable Grav debugger and following settings
+1 -1
View File
@@ -9,7 +9,7 @@
// Some standard defines // Some standard defines
define('GRAV', true); define('GRAV', true);
define('GRAV_VERSION', '1.7.31'); define('GRAV_VERSION', '1.7.37.1');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1'); define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false); define('GRAV_TESTING', false);
+4 -3
View File
@@ -182,13 +182,14 @@ class Setup extends Data
// If no environment is set, make sure we get one (CLI or hostname). // If no environment is set, make sure we get one (CLI or hostname).
if (null === $environment) { if (null === $environment) {
if (defined('GRAV_CLI')) { if (defined('GRAV_CLI')) {
$request = null;
$uri = null;
$environment = 'cli'; $environment = 'cli';
} else { } else {
/** @var ServerRequestInterface $request */ /** @var ServerRequestInterface $request */
$request = $container['request']; $request = $container['request'];
$host = $request->getUri()->getHost(); $uri = $request->getUri();
$environment = $uri->getHost();
$environment = Utils::substrToString($host, ':');
} }
} }
+4 -4
View File
@@ -515,7 +515,7 @@ class Blueprint extends BlueprintForm
$success = $this->resolveActions($user, $actions); $success = $this->resolveActions($user, $actions);
} }
if (!$success) { if (!$success) {
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]); static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
} }
} }
@@ -566,7 +566,7 @@ class Blueprint extends BlueprintForm
} }
if ($matches) { if ($matches) {
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]); static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
return; return;
} }
} }
@@ -577,7 +577,7 @@ class Blueprint extends BlueprintForm
* @param mixed $value * @param mixed $value
* @return void * @return void
*/ */
protected function addPropertyRecursive(array &$field, $property, $value) public static function addPropertyRecursive(array &$field, $property, $value)
{ {
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value); $field[$property] = array_merge_recursive($field[$property], $value);
@@ -587,7 +587,7 @@ class Blueprint extends BlueprintForm
if (!empty($field['fields'])) { if (!empty($field['fields'])) {
foreach ($field['fields'] as $key => &$child) { foreach ($field['fields'] as $key => &$child) {
$this->addPropertyRecursive($child, $property, $value); static::addPropertyRecursive($child, $property, $value);
} }
} }
} }
+20 -2
View File
@@ -246,7 +246,9 @@ class Validation
return false; return false;
} }
$max = (int)($params['max'] ?? 0); $multiline = isset($params['multiline']) && $params['multiline'];
$max = (int)($params['max'] ?? ($multiline ? 65536 : 2048));
if ($max && $len > $max) { if ($max && $len > $max) {
return false; return false;
} }
@@ -256,7 +258,7 @@ class Validation
return false; return false;
} }
if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) { if (!$multiline && preg_match('/\R/um', $value)) {
return false; return false;
} }
@@ -317,6 +319,10 @@ class Validation
*/ */
public static function typeCommaList($value, array $params, array $field) public static function typeCommaList($value, array $params, array $field)
{ {
if (!isset($params['max'])) {
$params['max'] = 2048;
}
return is_array($value) ? true : self::typeText($value, $params, $field); return is_array($value) ? true : self::typeText($value, $params, $field);
} }
@@ -379,6 +385,10 @@ class Validation
*/ */
public static function typePassword($value, array $params, array $field) public static function typePassword($value, array $params, array $field)
{ {
if (!isset($params['max'])) {
$params['max'] = 256;
}
return self::typeText($value, $params, $field); return self::typeText($value, $params, $field);
} }
@@ -621,6 +631,10 @@ class Validation
*/ */
public static function typeEmail($value, array $params, array $field) public static function typeEmail($value, array $params, array $field)
{ {
if (!isset($params['max'])) {
$params['max'] = 320;
}
$values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value; $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
foreach ($values as $val) { foreach ($values as $val) {
@@ -642,6 +656,10 @@ class Validation
*/ */
public static function typeUrl($value, array $params, array $field) public static function typeUrl($value, array $params, array $field)
{ {
if (!isset($params['max'])) {
$params['max'] = 2048;
}
return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL); return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
} }
@@ -43,4 +43,25 @@ class SystemFacade extends \Whoops\Util\SystemFacade
$handler(); $handler();
} }
} }
/**
* @param int $httpCode
*
* @return int
*/
public function setHttpResponseCode($httpCode)
{
if (!headers_sent()) {
// Ensure that no 'location' header is present as otherwise this
// will override the HTTP code being set here, and mask the
// expected error page.
header_remove('location');
// Work around PHP bug #8218 (8.0.17 & 8.1.4).
header_remove('Content-Encoding');
}
return http_response_code($httpCode);
}
} }
+81 -9
View File
@@ -10,6 +10,8 @@
namespace Grav\Common\File; namespace Grav\Common\File;
use Exception; use Exception;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Utils; use Grav\Common\Utils;
use RocketTheme\Toolbox\File\PhpFile; use RocketTheme\Toolbox\File\PhpFile;
use RuntimeException; use RuntimeException;
@@ -32,9 +34,10 @@ trait CompiledFile
public function content($var = null) public function content($var = null)
{ {
try { try {
$filename = $this->filename;
// If nothing has been loaded, attempt to get pre-compiled version of the file first. // If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) { if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($this->filename); $key = md5($filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified(); $modified = $this->modified();
@@ -48,39 +51,49 @@ trait CompiledFile
$class = get_class($this); $class = get_class($this);
$size = filesize($filename);
$cache = $file->exists() ? $file->content() : null; $cache = $file->exists() ? $file->content() : null;
// Load real file if cache isn't up to date (or is invalid). // Load real file if cache isn't up to date (or is invalid).
if (!isset($cache['@class']) if (!isset($cache['@class'])
|| $cache['@class'] !== $class || $cache['@class'] !== $class
|| $cache['modified'] !== $modified || $cache['modified'] !== $modified
|| $cache['filename'] !== $this->filename || ($cache['size'] ?? null) !== $size
|| $cache['filename'] !== $filename
) { ) {
// Attempt to lock the file for writing. // Attempt to lock the file for writing.
try { try {
$file->lock(false); $locked = $file->lock(false);
} catch (Exception $e) { } catch (Exception $e) {
// Another process has locked the file; we will check this in a bit. $locked = false;
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
} }
// Decode RAW file into compiled array. // Decode RAW file into compiled array.
$data = (array)$this->decode($this->raw()); $data = (array)$this->decode($this->raw());
$cache = [ $cache = [
'@class' => $class, '@class' => $class,
'filename' => $this->filename, 'filename' => $filename,
'modified' => $modified, 'modified' => $modified,
'size' => $size,
'data' => $data 'data' => $data
]; ];
// If compiled file wasn't already locked by another process, save it. // If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) { if ($locked) {
$file->save($cache); $file->save($cache);
$file->unlock(); $file->unlock();
// Compile cached file into bytecode cache // Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) { if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
$lockName = $file->filename();
// Silence error if function exists, but is restricted. // Silence error if function exists, but is restricted.
@opcache_invalidate($file->filename(), true); @opcache_invalidate($lockName, true);
@opcache_compile_file($lockName);
} }
} }
} }
@@ -89,12 +102,71 @@ trait CompiledFile
$this->content = $cache['data']; $this->content = $cache['data'];
} }
} catch (Exception $e) { } catch (Exception $e) {
throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($this->filename), $e->getMessage()), 500, $e); throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($filename), $e->getMessage()), 500, $e);
} }
return parent::content($var); return parent::content($var);
} }
/**
* Save file.
*
* @param mixed $data Optional data to be saved, usually array.
* @return void
* @throws RuntimeException
*/
public function save($data = null)
{
// Make sure that the cache file is always up to date!
$key = md5($this->filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
try {
$locked = $file->lock();
} catch (Exception $e) {
$locked = false;
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
}
parent::save($data);
if ($locked) {
$modified = $this->modified();
$filename = $this->filename;
$class = get_class($this);
$size = filesize($filename);
// windows doesn't play nicely with this as it can't read when locked
if (!Utils::isWindows()) {
// Reload data from the filesystem. This ensures that we always cache the correct data (see issue #2282).
$this->raw = $this->content = null;
$data = (array)$this->decode($this->raw());
}
// Decode data into compiled array.
$cache = [
'@class' => $class,
'filename' => $filename,
'modified' => $modified,
'size' => $size,
'data' => $data
];
$file->save($cache);
$file->unlock();
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
$lockName = $file->filename();
// Silence error if function exists, but is restricted.
@opcache_invalidate($lockName, true);
@opcache_compile_file($lockName);
}
}
}
/** /**
* Serialize file. * Serialize file.
* *
+58 -52
View File
@@ -31,32 +31,34 @@ abstract class Folder
/** /**
* Recursively find the last modified time under given path. * Recursively find the last modified time under given path.
* *
* @param string $path * @param array $paths
* @return int * @return int
*/ */
public static function lastModifiedFolder($path) public static function lastModifiedFolder(array $paths): int
{ {
if (!file_exists($path)) {
return 0;
}
$last_modified = 0; $last_modified = 0;
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = Grav::instance()['locator'];
$flags = RecursiveDirectoryIterator::SKIP_DOTS; $flags = RecursiveDirectoryIterator::SKIP_DOTS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$filter = new RecursiveFolderFilterIterator($directory);
$iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $dir) { foreach ($paths as $path) {
$dir_modified = $dir->getMTime(); if (!file_exists($path)) {
if ($dir_modified > $last_modified) { return 0;
$last_modified = $dir_modified; }
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$filter = new RecursiveFolderFilterIterator($directory);
$iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $dir) {
$dir_modified = $dir->getMTime();
if ($dir_modified > $last_modified) {
$last_modified = $dir_modified;
}
} }
} }
@@ -66,38 +68,40 @@ abstract class Folder
/** /**
* Recursively find the last modified time under given path by file. * Recursively find the last modified time under given path by file.
* *
* @param string $path * @param array $paths
* @param string $extensions which files to search for specifically * @param string $extensions which files to search for specifically
* @return int * @return int
*/ */
public static function lastModifiedFile($path, $extensions = 'md|yaml') public static function lastModifiedFile(array $paths, $extensions = 'md|yaml'): int
{ {
if (!file_exists($path)) {
return 0;
}
$last_modified = 0; $last_modified = 0;
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = Grav::instance()['locator'];
$flags = RecursiveDirectoryIterator::SKIP_DOTS; $flags = RecursiveDirectoryIterator::SKIP_DOTS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
/** @var RecursiveDirectoryIterator $file */ foreach($paths as $path) {
foreach ($iterator as $filepath => $file) { if (!file_exists($path)) {
try { return 0;
$file_modified = $file->getMTime(); }
if ($file_modified > $last_modified) { if ($locator->isStream($path)) {
$last_modified = $file_modified; $directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
/** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $file) {
try {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;
}
} catch (Exception $e) {
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
} }
} catch (Exception $e) {
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
} }
} }
@@ -107,28 +111,30 @@ abstract class Folder
/** /**
* Recursively md5 hash all files in a path * Recursively md5 hash all files in a path
* *
* @param string $path * @param array $paths
* @return string * @return string
*/ */
public static function hashAllFiles($path) public static function hashAllFiles(array $paths): string
{ {
$files = []; $files = [];
if (file_exists($path)) { foreach ($paths as $path) {
$flags = RecursiveDirectoryIterator::SKIP_DOTS; if (file_exists($path)) {
$flags = RecursiveDirectoryIterator::SKIP_DOTS;
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = Grav::instance()['locator'];
if ($locator->isStream($path)) { if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags); $directory = $locator->getRecursiveIterator($path, $flags);
} else { } else {
$directory = new RecursiveDirectoryIterator($path, $flags); $directory = new RecursiveDirectoryIterator($path, $flags);
} }
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) { foreach ($iterator as $file) {
$files[] = $file->getPathname() . '?'. $file->getMTime(); $files[] = $file->getPathname() . '?'. $file->getMTime();
}
} }
} }
@@ -454,7 +454,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
continue; continue;
} }
// Get the main key without template and langauge. // Get the main key without template and language.
[$main_key,] = explode('|', $entry['storage_key'] . '|', 2); [$main_key,] = explode('|', $entry['storage_key'] . '|', 2);
// Update storage key and language. // Update storage key and language.
@@ -527,10 +527,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$language = $options['lang']; $language = $options['lang'];
$status = 'error'; $status = 'error';
$msg = null;
$response = []; $response = [];
$children = null;
$sub_route = null;
$extra = null; $extra = null;
// Handle leaf_route // Handle leaf_route
@@ -610,12 +607,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$children = $page->children(); $children = $page->children();
/** @var PageIndex $children */ /** @var PageIndex $children */
$children = $children->getIndex(); $children = $children->getIndex();
$selectedChildren = $children->filterBy($filters, true); $selectedChildren = $children->filterBy($filters + ['language' => $language], true);
/** @var Header $header */ /** @var Header $header */
$header = $page->header(); $header = $page->header();
if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) { if (!$field && $header->get('admin.children_display_order', 'collection') === 'collection' && ($orderby = $header->get('content.order.by'))) {
// Use custom sorting by page header. // Use custom sorting by page header.
$sortby = $orderby; $sortby = $orderby;
$order = $header->get('content.order.dir', $order); $order = $header->get('content.order.dir', $order);
@@ -242,6 +242,7 @@ class PageObject extends FlexPageObject
{ {
/** @var PageCollection $siblings */ /** @var PageCollection $siblings */
$siblings = $variables['siblings']; $siblings = $variables['siblings'];
/** @var PageObject $sibling */
foreach ($siblings as $sibling) { foreach ($siblings as $sibling) {
$sibling->save(false); $sibling->save(false);
} }
@@ -585,38 +586,46 @@ class PageObject extends FlexPageObject
*/ */
public function filterBy(array $filters, bool $recursive = false): bool public function filterBy(array $filters, bool $recursive = false): bool
{ {
$language = $filters['language'] ?? null;
if (null !== $language) {
/** @var PageObject $test */
$test = $this->getTranslation($language) ?? $this;
} else {
$test = $this;
}
foreach ($filters as $key => $value) { foreach ($filters as $key => $value) {
switch ($key) { switch ($key) {
case 'search': case 'search':
$matches = $this->search((string)$value) > 0.0; $matches = $test->search((string)$value) > 0.0;
break; break;
case 'page_type': case 'page_type':
$types = $value ? explode(',', $value) : []; $types = $value ? explode(',', $value) : [];
$matches = in_array($this->template(), $types, true); $matches = in_array($test->template(), $types, true);
break; break;
case 'extension': case 'extension':
$matches = Utils::contains((string)$value, $this->extension()); $matches = Utils::contains((string)$value, $test->extension());
break; break;
case 'routable': case 'routable':
$matches = $this->isRoutable() === (bool)$value; $matches = $test->isRoutable() === (bool)$value;
break; break;
case 'published': case 'published':
$matches = $this->isPublished() === (bool)$value; $matches = $test->isPublished() === (bool)$value;
break; break;
case 'visible': case 'visible':
$matches = $this->isVisible() === (bool)$value; $matches = $test->isVisible() === (bool)$value;
break; break;
case 'module': case 'module':
$matches = $this->isModule() === (bool)$value; $matches = $test->isModule() === (bool)$value;
break; break;
case 'page': case 'page':
$matches = $this->isPage() === (bool)$value; $matches = $test->isPage() === (bool)$value;
break; break;
case 'folder': case 'folder':
$matches = $this->isPage() === !$value; $matches = $test->isPage() === !$value;
break; break;
case 'translated': case 'translated':
$matches = $this->hasTranslation() === (bool)$value; $matches = $test->hasTranslation() === (bool)$value;
break; break;
default: default:
$matches = true; $matches = true;
@@ -30,7 +30,7 @@ use function is_string;
*/ */
class UserIndex extends FlexIndex implements UserCollectionInterface class UserIndex extends FlexIndex implements UserCollectionInterface
{ {
public const VERSION = parent::VERSION . '.1'; public const VERSION = parent::VERSION . '.2';
/** /**
* @param FlexStorageInterface $storage * @param FlexStorageInterface $storage
@@ -50,7 +50,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
// return $index['index']; // return $index['index'];
//} //}
// Load up to date index. // Load up-to-date index.
$entries = parent::loadEntriesFromStorage($storage); $entries = parent::loadEntriesFromStorage($storage);
return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]); return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]);
@@ -142,9 +142,11 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
} elseif ($field === 'flex_key') { } elseif ($field === 'flex_key') {
$user = $this->withKeyField('flex_key')->get($query); $user = $this->withKeyField('flex_key')->get($query);
} elseif ($field === 'email') { } elseif ($field === 'email') {
$user = $this->withKeyField('email')->get($query); $email = mb_strtolower($query);
$user = $this->withKeyField('email')->get($email);
} elseif ($field === 'username') { } elseif ($field === 'username') {
$user = $this->get(static::filterUsername($query, $this->getFlexDirectory()->getStorage())); $username = static::filterUsername($query, $this->getFlexDirectory()->getStorage());
$user = $this->get($username);
} else { } else {
$user = $this->__call('find', [$query, $field]); $user = $this->__call('find', [$query, $field]);
} }
@@ -31,6 +31,7 @@ use Grav\Common\Flex\Types\UserGroups\UserGroupIndex;
use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait; use Grav\Common\User\Traits\UserTrait;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface;
use Grav\Framework\File\Formatter\JsonFormatter; use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter; use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Filesystem\Filesystem;
@@ -38,7 +39,10 @@ use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Storage\FileStorage; use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexMediaTrait; use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Flex\Traits\FlexRelationshipsTrait;
use Grav\Framework\Form\FormFlashFile; use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Media\MediaIdentifier;
use Grav\Framework\Media\UploadedMediaObject;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\FileInterface; use RocketTheme\Toolbox\File\FileInterface;
@@ -77,6 +81,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
} }
use UserTrait; use UserTrait;
use UserObjectLegacyTrait; use UserObjectLegacyTrait;
use FlexRelationshipsTrait;
/** @var Closure|null */ /** @var Closure|null */
static public $authorizeCallable; static public $authorizeCallable;
@@ -672,6 +677,81 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $folder; return $folder;
} }
/**
* @param string $name
* @return array|object|null
* @internal
*/
public function initRelationship(string $name)
{
switch ($name) {
case 'media':
$list = [];
foreach ($this->getMedia()->all() as $filename => $object) {
$list[] = $this->buildMediaObject(null, $filename, $object);
}
return $list;
case 'avatar':
return $this->buildMediaObject('avatar', basename($this->getAvatarUrl()), $this->getAvatarImage());
}
throw new \InvalidArgumentException(sprintf('%s: Relationship %s does not exist', $this->getFlexType(), $name));
}
/**
* @return bool Return true if relationships were updated.
*/
protected function updateRelationships(): bool
{
$modified = $this->getRelationships()->getModified();
if ($modified) {
foreach ($modified as $relationship) {
$name = $relationship->getName();
switch ($name) {
case 'avatar':
\assert($relationship instanceof ToOneRelationshipInterface);
$this->updateAvatarRelationship($relationship);
break;
default:
throw new \InvalidArgumentException(sprintf('%s: Relationship %s cannot be modified', $this->getFlexType(), $name), 400);
}
}
$this->resetRelationships();
return true;
}
return false;
}
/**
* @param ToOneRelationshipInterface $relationship
*/
protected function updateAvatarRelationship(ToOneRelationshipInterface $relationship): void
{
$files = [];
$avatar = $this->getAvatarImage();
if ($avatar) {
$files['avatar'][$avatar->filename] = null;
}
$identifier = $relationship->getIdentifier();
if ($identifier) {
\assert($identifier instanceof MediaIdentifier);
$object = $identifier->getObject();
if ($object instanceof UploadedMediaObject) {
$uploadedFile = $object->getUploadedFile();
if ($uploadedFile) {
$files['avatar'][$uploadedFile->getClientFilename()] = $uploadedFile;
}
}
}
$this->update([], $files);
}
/** /**
* @param string $name * @param string $name
* @return Blueprint * @return Blueprint
+1
View File
@@ -12,6 +12,7 @@ namespace Grav\Common\GPM;
use Exception; use Exception;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\HTTP\Response;
use Grav\Common\Inflector; use Grav\Common\Inflector;
use Grav\Common\Iterator; use Grav\Common\Iterator;
use Grav\Common\Utils; use Grav\Common\Utils;
@@ -10,8 +10,8 @@
namespace Grav\Common\GPM\Remote; namespace Grav\Common\GPM\Remote;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\HTTP\Response;
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection; use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
use Grav\Common\GPM\Response;
use \Doctrine\Common\Cache\FilesystemCache; use \Doctrine\Common\Cache\FilesystemCache;
use RuntimeException; use RuntimeException;
+24 -5
View File
@@ -48,6 +48,7 @@ use Grav\Common\Service\TaskServiceProvider;
use Grav\Common\Twig\Twig; use Grav\Common\Twig\Twig;
use Grav\Framework\DI\Container; use Grav\Framework\DI\Container;
use Grav\Framework\Psr7\Response; use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Middlewares\MultipartRequestSupport;
use Grav\Framework\RequestHandler\RequestHandler; use Grav\Framework\RequestHandler\RequestHandler;
use Grav\Framework\Route\Route; use Grav\Framework\Route\Route;
use Grav\Framework\Session\Messages; use Grav\Framework\Session\Messages;
@@ -117,6 +118,7 @@ class Grav extends Container
* @var array All middleware processors that are processed in $this->process() * @var array All middleware processors that are processed in $this->process()
*/ */
protected $middleware = [ protected $middleware = [
'multipartRequestSupport',
'initializeProcessor', 'initializeProcessor',
'pluginsProcessor', 'pluginsProcessor',
'themesProcessor', 'themesProcessor',
@@ -259,6 +261,9 @@ class Grav extends Container
$container = new Container( $container = new Container(
[ [
'multipartRequestSupport' => function () {
return new MultipartRequestSupport();
},
'initializeProcessor' => function () { 'initializeProcessor' => function () {
return new InitializeProcessor($this); return new InitializeProcessor($this);
}, },
@@ -341,6 +346,23 @@ class Grav extends Container
} }
} }
/**
* Clean any output buffers. Useful when exiting from the application.
*
* Please use $grav->close() and $grav->redirect() instead of calling this one!
*
* @return void
*/
public function cleanOutputBuffers(): void
{
// Make sure nothing extra gets written to the response.
while (ob_get_level()) {
ob_end_clean();
}
// Work around PHP bug #8218 (8.0.17 & 8.1.4).
header_remove('Content-Encoding');
}
/** /**
* Terminates Grav request with a response. * Terminates Grav request with a response.
* *
@@ -351,10 +373,7 @@ class Grav extends Container
*/ */
public function close(ResponseInterface $response): void public function close(ResponseInterface $response): void
{ {
// Make sure nothing extra gets written to the response. $this->cleanOutputBuffers();
while (ob_get_level()) {
ob_end_clean();
}
// Close the session. // Close the session.
if (isset($this['session'])) { if (isset($this['session'])) {
@@ -400,7 +419,7 @@ class Grav extends Container
/** /**
* @param ResponseInterface $response * @param ResponseInterface $response
* @return never-return * @return never-return
* @deprecated 1.7 Do not use * @deprecated 1.7 Use $grav->close() instead.
*/ */
public function exit(ResponseInterface $response): void public function exit(ResponseInterface $response): void
{ {
@@ -153,6 +153,8 @@ class LanguageCodes
'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ], 'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ],
'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ], 'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ],
'xh' => [ 'name' => 'Xhosa', 'nativeName' => 'isiXhosa' ], 'xh' => [ 'name' => 'Xhosa', 'nativeName' => 'isiXhosa' ],
'yi' => [ 'name' => 'Yiddish', 'nativeName' => 'ייִדיש', 'orientation' => 'rtl' ],
'ydd' => [ 'name' => 'Yiddish', 'nativeName' => 'ייִדיש', 'orientation' => 'rtl' ],
'zh' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], 'zh' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ],
'zh-CN' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], 'zh-CN' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ],
'zh-TW' => [ 'name' => 'Chinese (Traditional)', 'nativeName' => '正體中文 (繁體)' ], 'zh-TW' => [ 'name' => 'Chinese (Traditional)', 'nativeName' => '正體中文 (繁體)' ],
@@ -158,14 +158,6 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj
*/ */
public function thumbnail($type = 'auto'); public function thumbnail($type = 'auto');
/**
* Return URL to file.
*
* @param bool $reset
* @return string
*/
public function url($reset = true);
/** /**
* Turn the current Medium into a Link * Turn the current Medium into a Link
* *
@@ -221,18 +213,6 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function __call($method, $args); public function __call($method, $args);
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $this->get('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string|null $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function get($name, $default = null, $separator = null);
/** /**
* Set value by using dot notation for nested arrays/objects. * Set value by using dot notation for nested arrays/objects.
* *
+1 -1
View File
@@ -152,7 +152,7 @@ class Media extends AbstractMedia
foreach ($iterator as $file => $info) { foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files. // Ignore folders and Markdown files.
$filename = $info->getFilename(); $filename = $info->getFilename();
if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) { if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || $filename === 'media.json' || strpos($filename, '.') === 0) {
continue; continue;
} }
@@ -361,8 +361,8 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
// Scaling operations // Scaling operations
$scale = ($scale ?? $config->get('system.images.watermark.scale', 100)) / 100; $scale = ($scale ?? $config->get('system.images.watermark.scale', 100)) / 100;
$wwidth = $this->get('width') * $scale; $wwidth = (int)$this->get('width') * $scale;
$wheight = $this->get('height') * $scale; $wheight = (int)$this->get('height') * $scale;
$watermark->resize($wwidth, $wheight); $watermark->resize($wwidth, $wheight);
// Position operations // Position operations
@@ -377,11 +377,11 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
break; break;
case 'bottom': case 'bottom':
$positionY = $this->get('height')-$wheight; $positionY = (int)$this->get('height')-$wheight;
break; break;
case 'center': case 'center':
$positionY = ($this->get('height')/2) - ($wheight/2); $positionY = ((int)$this->get('height')/2) - ($wheight/2);
break; break;
} }
@@ -392,11 +392,11 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
break; break;
case 'right': case 'right':
$positionX = $this->get('width')-$wwidth; $positionX = (int)$this->get('width')-$wwidth;
break; break;
case 'center': case 'center':
$positionX = ($this->get('width')/2) - ($wwidth/2); $positionX = ((int)$this->get('width')/2) - ($wwidth/2);
break; break;
} }
+6 -1
View File
@@ -622,7 +622,12 @@ class Page implements PageInterface
$headers['Vary'] = 'Accept-Encoding'; $headers['Vary'] = 'Accept-Encoding';
} }
return $headers;
// Added new Headers event
$headers_obj = (object) $headers;
Grav::instance()->fireEvent('onPageHeaders', new Event(['headers' => $headers_obj]));
return (array)$headers_obj;
} }
/** /**
+49 -17
View File
@@ -88,6 +88,8 @@ class Pages
/** @var string */ /** @var string */
protected $check_method; protected $check_method;
/** @var string */ /** @var string */
protected $simple_pages_hash;
/** @var string */
protected $pages_cache_id; protected $pages_cache_id;
/** @var bool */ /** @var bool */
protected $initialized = false; protected $initialized = false;
@@ -100,6 +102,7 @@ class Pages
/** @var string|null */ /** @var string|null */
protected static $home_route; protected static $home_route;
/** /**
* Constructor * Constructor
* *
@@ -1712,10 +1715,7 @@ class Pages
/** @var Language $language */ /** @var Language $language */
$language = $this->grav['language']; $language = $this->grav['language'];
$pages_dir = $locator->findResource('page://'); $pages_dirs = $this->getPagesPaths();
if (!is_string($pages_dir)) {
throw new RuntimeException('Internal Error');
}
// Set active language // Set active language
$this->active_lang = $language->getActive(); $this->active_lang = $language->getActive();
@@ -1731,16 +1731,17 @@ class Pages
$hash = 0; $hash = 0;
break; break;
case 'folder': case 'folder':
$hash = Folder::lastModifiedFolder($pages_dir); $hash = Folder::lastModifiedFolder($pages_dirs);
break; break;
case 'hash': case 'hash':
$hash = Folder::hashAllFiles($pages_dir); $hash = Folder::hashAllFiles($pages_dirs);
break; break;
default: default:
$hash = Folder::lastModifiedFile($pages_dir); $hash = Folder::lastModifiedFile($pages_dirs);
} }
$this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum()); $this->simple_pages_hash = json_encode($pages_dirs) . $hash . $config->checksum();
$this->pages_cache_id = md5($this->simple_pages_hash . $language->getActive());
/** @var Cache $cache */ /** @var Cache $cache */
$cache = $this->grav['cache']; $cache = $this->grav['cache'];
@@ -1760,18 +1761,39 @@ class Pages
$this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..'); $this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..');
} }
$this->resetPages($pages_dir); $this->resetPages($pages_dirs);
}
protected function getPagesPaths(): array
{
$grav = Grav::instance();
$locator = $grav['locator'];
$paths = [];
$dirs = (array) $grav['config']->get('system.pages.dirs', ['page://']);
foreach ($dirs as $dir) {
$path = $locator->findResource($dir);
if (file_exists($path)) {
$paths[] = $path;
}
}
return $paths;
} }
/** /**
* Accessible method to manually reset the pages cache * Accessible method to manually reset the pages cache
* *
* @param string $pages_dir * @param array $pages_dirs
*/ */
public function resetPages($pages_dir): void public function resetPages(array $pages_dirs): void
{ {
$this->sort = []; $this->sort = [];
$this->recurse($pages_dir);
foreach ($pages_dirs as $dir) {
$this->recurse($dir);
}
$this->buildRoutes(); $this->buildRoutes();
// cache if needed // cache if needed
@@ -1795,7 +1817,7 @@ class Pages
* @throws RuntimeException * @throws RuntimeException
* @internal * @internal
*/ */
protected function recurse($directory, PageInterface $parent = null) protected function recurse(string $directory, PageInterface $parent = null)
{ {
$directory = rtrim($directory, DS); $directory = rtrim($directory, DS);
$page = new Page; $page = new Page;
@@ -2177,7 +2199,7 @@ class Pages
* @param array $list * @param array $list
* @return array * @return array
*/ */
protected function arrayShuffle($list) protected function arrayShuffle(array $list): array
{ {
$keys = array_keys($list); $keys = array_keys($list);
shuffle($keys); shuffle($keys);
@@ -2193,7 +2215,7 @@ class Pages
/** /**
* @return string * @return string
*/ */
protected function getVersion() protected function getVersion(): string
{ {
return $this->directory ? 'flex' : 'regular'; return $this->directory ? 'flex' : 'regular';
} }
@@ -2204,10 +2226,20 @@ class Pages
* this is particularly useful to know if pages have changed and you want * this is particularly useful to know if pages have changed and you want
* to sync another cache with pages cache - works best in `onPagesInitialized()` * to sync another cache with pages cache - works best in `onPagesInitialized()`
* *
* @return string * @return null|string
*/ */
public function getPagesCacheId() public function getPagesCacheId(): ?string
{ {
return $this->pages_cache_id; return $this->pages_cache_id;
} }
/**
* Get the simple pages hash that is not md5 encoded, and isn't specific to language
*
* @return null|string
*/
public function getSimplePagesHash(): ?string
{
return $this->simple_pages_hash;
}
} }
@@ -251,7 +251,8 @@ class InitializeProcessor extends ProcessorBase
$log->popHandler(); $log->popHandler();
$facility = $config->get('system.log.syslog.facility', 'local6'); $facility = $config->get('system.log.syslog.facility', 'local6');
$logHandler = new SyslogHandler('grav', $facility); $tag = $config->get('system.log.syslog.tag', 'grav');
$logHandler = new SyslogHandler($tag, $facility);
$formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
$logHandler->setFormatter($formatter); $logHandler->setFormatter($formatter);
+14 -16
View File
@@ -97,7 +97,7 @@ class Security
*/ */
public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null) public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null)
{ {
$routes = $pages->routes(); $routes = $pages->getList(null, 0, true);
// Remove duplicate for homepage // Remove duplicate for homepage
unset($routes['/']); unset($routes['/']);
@@ -110,26 +110,23 @@ class Security
'steps' => count($routes), 'steps' => count($routes),
]); ]);
foreach ($routes as $path) { foreach (array_keys($routes) as $route) {
$status && $status([ $status && $status([
'type' => 'progress', 'type' => 'progress',
]); ]);
try { try {
$page = $pages->get($path); $page = $pages->find($route);
if ($page->exists()) {
// call the content to load/cache it
$header = (array) $page->header();
$content = $page->value('content');
// call the content to load/cache it $data = ['header' => $header, 'content' => $content];
$header = (array) $page->header(); $results = static::detectXssFromArray($data);
$content = $page->value('content');
$data = ['header' => $header, 'content' => $content]; if (!empty($results)) {
$results = static::detectXssFromArray($data); $list[$page->rawRoute()] = $results;
if (!empty($results)) {
if ($route) {
$list[$page->route()] = $results;
} else {
$list[$page->filePathClean()] = $results;
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
@@ -222,7 +219,8 @@ class Security
$string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8'); $string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
// Strip whitespace characters // Strip whitespace characters
$string = preg_replace('!\s!u', '', $string); $string = preg_replace('!\s!u', ' ', $string);
$stripped = preg_replace('!\s!u', '', $string);
// Set the patterns we'll test against // Set the patterns we'll test against
$patterns = [ $patterns = [
@@ -245,7 +243,7 @@ class Security
// Iterate over rules and return label if fail // Iterate over rules and return label if fail
foreach ($patterns as $name => $regex) { foreach ($patterns as $name => $regex) {
if (!empty($enabled_rules[$name])) { if (!empty($enabled_rules[$name])) {
if (preg_match($regex, $string) || preg_match($regex, $orig)) { if (preg_match($regex, $string) || preg_match($regex, $stripped) || preg_match($regex, $orig)) {
return $name; return $name;
} }
} }
@@ -97,7 +97,9 @@ class FlexServiceProvider implements ServiceProviderInterface
'options' => [ 'options' => [
'file' => 'user', 'file' => 'user',
'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}', 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}',
'key' => 'storage_key' 'key' => 'storage_key',
'indexed' => true,
'case_sensitive' => false
], ],
]; ];
} }
@@ -107,7 +109,9 @@ class FlexServiceProvider implements ServiceProviderInterface
'class' => UserFileStorage::class, 'class' => UserFileStorage::class,
'options' => [ 'options' => [
'pattern' => '{FOLDER}/{KEY}{EXT}', 'pattern' => '{FOLDER}/{KEY}{EXT}',
'key' => 'username' 'key' => 'username',
'indexed' => true,
'case_sensitive' => false
], ],
]; ];
} }
@@ -9,6 +9,7 @@
namespace Grav\Common\Twig\Extension; namespace Grav\Common\Twig\Extension;
use CallbackFilterIterator;
use Cron\CronExpression; use Cron\CronExpression;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Grav\Common\Data\Data; use Grav\Common\Data\Data;
@@ -41,6 +42,7 @@ use JsonSerializable;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Traversable; use Traversable;
use Twig\Environment; use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface; use Twig\Extension\GlobalsInterface;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
@@ -145,6 +147,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']), new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']), new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
new TwigFilter('nicecron', [$this, 'niceCronFilter']), new TwigFilter('nicecron', [$this, 'niceCronFilter']),
new TwigFilter('replace_last', [$this, 'replaceLastFilter']),
// Translations // Translations
new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]), new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]),
@@ -166,6 +169,9 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
// PHP methods // PHP methods
new TwigFilter('count', 'count'), new TwigFilter('count', 'count'),
new TwigFilter('array_diff', 'array_diff'), new TwigFilter('array_diff', 'array_diff'),
// Security fix
new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]),
]; ];
} }
@@ -194,6 +200,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
new TwigFunction('gist', [$this, 'gistFunc']), new TwigFunction('gist', [$this, 'gistFunc']),
new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']), new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']),
new TwigFunction('pathinfo', 'pathinfo'), new TwigFunction('pathinfo', 'pathinfo'),
new TwigFunction('parseurl', 'parse_url'),
new TwigFunction('random_string', [$this, 'randomStringFunc']), new TwigFunction('random_string', [$this, 'randomStringFunc']),
new TwigFunction('repeat', [$this, 'repeatFunc']), new TwigFunction('repeat', [$this, 'repeatFunc']),
new TwigFunction('regex_replace', [$this, 'regexReplace']), new TwigFunction('regex_replace', [$this, 'regexReplace']),
@@ -547,6 +554,21 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
return $cron->getText('en'); return $cron->getText('en');
} }
/**
* @param string|mixed $str
* @param string $search
* @param string $replace
* @return string|mixed
*/
public function replaceLastFilter($str, $search, $replace)
{
if (is_string($str) && ($pos = mb_strrpos($str, $search)) !== false) {
$str = mb_substr($str, 0, $pos) . $replace . mb_substr($str, $pos + mb_strlen($search));
}
return $str;
}
/** /**
* Get Cron object for a crontab 'at' format * Get Cron object for a crontab 'at' format
* *
@@ -1659,4 +1681,20 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
return is_string($var); return is_string($var);
} }
} }
/**
* @param Environment $env
* @param array $array
* @param callable|string $arrow
* @return array|CallbackFilterIterator
* @throws RuntimeError
*/
function filterFilter(Environment $env, $array, $arrow)
{
if (is_string($arrow) && Utils::isDangerousFunction($arrow)) {
throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
}
return twig_array_filter($env, $array, $arrow);
}
} }
@@ -112,11 +112,12 @@ class UserCollection implements UserCollectionInterface
// If not found, try the fields // If not found, try the fields
if (!$user->exists()) { if (!$user->exists()) {
$query = mb_strtolower($query);
foreach ($files as $file) { foreach ($files as $file) {
if (Utils::endsWith($file, YAML_EXT)) { if (Utils::endsWith($file, YAML_EXT)) {
$find_user = $this->load(trim(Utils::pathinfo($file, PATHINFO_FILENAME))); $find_user = $this->load(trim(Utils::pathinfo($file, PATHINFO_FILENAME)));
foreach ($fields as $field) { foreach ($fields as $field) {
if (isset($find_user[$field]) && $find_user[$field] === $query) { if (isset($find_user[$field]) && mb_strtolower($find_user[$field]) === $query) {
return $find_user; return $find_user;
} }
} }
+17 -7
View File
@@ -83,6 +83,7 @@ abstract class Utils
$resource = false; $resource = false;
if (static::contains((string)$input, '://')) { if (static::contains((string)$input, '://')) {
// Url contains a scheme (https:// , user:// etc).
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = $grav['locator']; $locator = $grav['locator'];
@@ -134,6 +135,16 @@ abstract class Utils
$resource = $locator->findResource($input, false); $resource = $locator->findResource($input, false);
} }
} else { } else {
// Just a path.
/** @var Pages $pages */
$pages = $grav['pages'];
// Is this a page?
$page = $pages->find($input, true);
if ($page && $page->routable()) {
return $page->url($domain);
}
$root = preg_quote($uri->rootUrl(), '#'); $root = preg_quote($uri->rootUrl(), '#');
$pattern = '#(' . $root . '$|' . $root . '/)#'; $pattern = '#(' . $root . '$|' . $root . '/)#';
if (!empty($root) && preg_match($pattern, $input, $matches)) { if (!empty($root) && preg_match($pattern, $input, $matches)) {
@@ -657,18 +668,17 @@ abstract class Utils
*/ */
public static function download($file, $force_download = true, $sec = 0, $bytes = 1024, array $options = []) public static function download($file, $force_download = true, $sec = 0, $bytes = 1024, array $options = [])
{ {
$grav = Grav::instance();
if (file_exists($file)) { if (file_exists($file)) {
// fire download event // fire download event
Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file, 'options' => &$options])); $grav->fireEvent('onBeforeDownload', new Event(['file' => $file, 'options' => &$options]));
$file_parts = static::pathinfo($file); $file_parts = static::pathinfo($file);
$mimetype = $options['mime'] ?? static::getMimeByExtension($file_parts['extension']); $mimetype = $options['mime'] ?? static::getMimeByExtension($file_parts['extension']);
$size = filesize($file); // File size $size = filesize($file); // File size
// clean all buffers $grav->cleanOutputBuffers();
while (ob_get_level()) {
ob_end_clean();
}
// required for IE, otherwise Content-Disposition may be ignored // required for IE, otherwise Content-Disposition may be ignored
if (ini_get('zlib.output_compression')) { if (ini_get('zlib.output_compression')) {
@@ -703,8 +713,8 @@ abstract class Utils
$new_length = $size; $new_length = $size;
header('Content-Length: ' . $size); header('Content-Length: ' . $size);
if (Grav::instance()['config']->get('system.cache.enabled')) { if ($grav['config']->get('system.cache.enabled')) {
$expires = $options['expires'] ?? Grav::instance()['config']->get('system.pages.expires'); $expires = $options['expires'] ?? $grav['config']->get('system.pages.expires');
if ($expires > 0) { if ($expires > 0) {
$expires_date = gmdate('D, d M Y H:i:s T', time() + $expires); $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
header('Cache-Control: max-age=' . $expires); header('Cache-Control: max-age=' . $expires);
@@ -147,7 +147,7 @@ class InstallCommand extends GravCommand
foreach ($this->config['git'] as $repo => $data) { foreach ($this->config['git'] as $repo => $data) {
$path = $this->destination . DS . $data['path']; $path = $this->destination . DS . $data['path'];
if (!file_exists($path)) { if (!file_exists($path)) {
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return); exec('cd ' . escapeshellarg($this->destination) . ' && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return);
if (!$return) { if (!$return) {
$io->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>'); $io->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
+1 -1
View File
@@ -288,7 +288,7 @@ trait ConsoleTrait
{ {
$composer = Composer::getComposerExecutor(); $composer = Composer::getComposerExecutor();
return system($composer . ' --working-dir="'.$path.'" --no-interaction --no-dev --prefer-dist -o '. $action); return system($composer . ' --working-dir=' . escapeshellarg($path) . ' --no-interaction --no-dev --prefer-dist -o '. $action);
} }
/** /**
@@ -10,12 +10,11 @@
namespace Grav\Console\Gpm; namespace Grav\Console\Gpm;
use Exception; use Exception;
use Grav\Common\Cache;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\HTTP\Response;
use Grav\Common\GPM\GPM; use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer; use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Console\GpmCommand; use Grav\Console\GpmCommand;
use RuntimeException; use RuntimeException;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -11,10 +11,10 @@ namespace Grav\Console\Gpm;
use Exception; use Exception;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\HTTP\Response;
use Grav\Common\GPM\GPM; use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer; use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Licenses; use Grav\Common\GPM\Licenses;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Remote\Package; use Grav\Common\GPM\Remote\Package;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Utils; use Grav\Common\Utils;
@@ -485,7 +485,7 @@ class InstallCommand extends GpmCommand
{ {
$io = $this->getIO(); $io = $this->getIO();
exec('cd ' . $this->destination); exec('cd ' . escapeshellarg($this->destination));
$to = $this->destination . DS . $package->install_path; $to = $this->destination . DS . $package->install_path;
$from = $this->getSymlinkSource($package); $from = $this->getSymlinkSource($package);
@@ -11,8 +11,8 @@ namespace Grav\Console\Gpm;
use Exception; use Exception;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\HTTP\Response;
use Grav\Common\GPM\Installer; use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Upgrader; use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Console\GpmCommand; use Grav\Console\GpmCommand;
@@ -157,9 +157,6 @@ trait CacheTrait
$this->validateKeys($keys); $this->validateKeys($keys);
$keys = array_unique($keys); $keys = array_unique($keys);
$keys = array_combine($keys, $keys); $keys = array_combine($keys, $keys);
if (empty($keys)) {
return [];
}
$list = $this->doGetMultiple($keys, $this->miss); $list = $this->doGetMultiple($keys, $this->miss);
@@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Media;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Media Object Interface
*/
interface MediaObjectInterface extends IdentifierInterface
{
/**
* Returns true if the object exists.
*
* @return bool
* @phpstan-pure
*/
public function exists(): bool;
/**
* Get metadata associated to the media object.
*
* @return array
* @phpstan-pure
*/
public function getMeta(): array;
/**
* @param string $field
* @return mixed
* @phpstan-pure
*/
public function get(string $field);
/**
* Return URL pointing to the media object.
*
* @return string
* @phpstan-pure
*/
public function getUrl(): string;
/**
* Create media response.
*
* @param array $actions
* @return ResponseInterface
* @phpstan-pure
*/
public function createResponse(array $actions): ResponseInterface;
}
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Object;
use JsonSerializable;
/**
* Interface IdentifierInterface
*/
interface IdentifierInterface extends JsonSerializable
{
/**
* Get identifier's ID.
*
* @return string
* @phpstan-pure
*/
public function getId(): string;
/**
* Get identifier's type.
*
* @return string
* @phpstan-pure
*/
public function getType(): string;
}
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface RelationshipIdentifierInterface
*/
interface RelationshipIdentifierInterface extends IdentifierInterface
{
/**
* If identifier has meta.
*
* @return bool
* @phpstan-pure
*/
public function hasIdentifierMeta(): bool;
/**
* Get identifier meta.
*
* @return array<string,mixed>|ArrayAccess<string,mixed>
* @phpstan-pure
*/
public function getIdentifierMeta();
}
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Countable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use IteratorAggregate;
use JsonSerializable;
use Serializable;
/**
* Interface Relationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @extends IteratorAggregate<string, T>
*/
interface RelationshipInterface extends Countable, IteratorAggregate, JsonSerializable, Serializable
{
/**
* @return string
* @phpstan-pure
*/
public function getName(): string;
/**
* @return string
* @phpstan-pure
*/
public function getType(): string;
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string;
/**
* @return P
* @phpstan-pure
*/
public function getParent(): IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool;
/**
* @param T $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool;
/**
* @return iterable<T>
*/
public function getIterator(): iterable;
}
@@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface RelationshipsInterface
*
* @template T of \Grav\Framework\Contracts\Object\IdentifierInterface
* @template P of \Grav\Framework\Contracts\Object\IdentifierInterface
* @extends ArrayAccess<string,RelationshipInterface<T,P>>
* @extends Iterator<string,RelationshipInterface<T,P>>
*/
interface RelationshipsInterface extends Countable, ArrayAccess, Iterator, JsonSerializable
{
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return array
*/
public function getModified(): array;
/**
* @return int
* @phpstan-pure
*/
public function count(): int;
/**
* @param string $offset
* @return RelationshipInterface<T,P>|null
*/
public function offsetGet($offset): ?RelationshipInterface;
/**
* @return RelationshipInterface<T,P>|null
*/
public function current(): ?RelationshipInterface;
/**
* @return string
* @phpstan-pure
*/
public function key(): string;
}
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToManyRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToManyRelationshipInterface extends RelationshipInterface
{
/**
* @param positive-int $pos
* @return IdentifierInterface|null
*/
public function getNthIdentifier(int $pos): ?IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id, string $type = null): ?object;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool;
}
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToOneRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToOneRelationshipInterface extends RelationshipInterface
{
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface;
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id = null, string $type = null): ?object;
/**
* @param T|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool;
}
@@ -147,6 +147,10 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
*/ */
public function search(string $search, $properties = null, array $options = null) public function search(string $search, $properties = null, array $options = null)
{ {
$directory = $this->getFlexDirectory();
$properties = $directory->getSearchProperties($properties);
$options = $directory->getSearchOptions($options);
$matching = $this->call('search', [$search, $properties, $options]); $matching = $this->call('search', [$search, $properties, $options]);
$matching = array_filter($matching); $matching = array_filter($matching);
@@ -170,6 +170,44 @@ class FlexDirectory implements FlexDirectoryInterface
return null === $name ? $this->config : $this->config->get($name, $default); return null === $name ? $this->config : $this->config->get($name, $default);
} }
/**
* @param string|string[]|null $properties
* @return array
*/
public function getSearchProperties($properties = null): array
{
if (null !== $properties) {
return (array)$properties;
}
$properties = $this->getConfig('data.search.fields');
if (!$properties) {
$fields = $this->getConfig('admin.views.list.fields') ?? $this->getConfig('admin.list.fields', []);
foreach ($fields as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
return $properties;
}
/**
* @param array|null $options
* @return array
*/
public function getSearchOptions(array $options = null): array
{
if (empty($options['merge'])) {
return $options ?? (array)$this->getConfig('data.search.options');
}
unset($options['merge']);
return $options + (array)$this->getConfig('data.search.options');
}
/** /**
* @param string|null $name * @param string|null $name
* @param array $options * @param array $options
@@ -750,6 +788,7 @@ class FlexDirectory implements FlexDirectoryInterface
public function reloadIndex(): void public function reloadIndex(): void
{ {
$this->getCache('index')->clear(); $this->getCache('index')->clear();
$this->getIndex()::loadEntriesFromStorage($this->getStorage());
$this->indexes = []; $this->indexes = [];
$this->objects = []; $this->objects = [];
@@ -797,6 +836,9 @@ class FlexDirectory implements FlexDirectoryInterface
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) { $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
$this->dynamicFlexField($field, $property, $call); $this->dynamicFlexField($field, $property, $call);
}); });
$blueprint->addDynamicHandler('authorize', function (array &$field, $property, array &$call) {
$this->dynamicAuthorizeField($field, $property, $call);
});
if ($context) { if ($context) {
$blueprint->setContext($context); $blueprint->setContext($context);
@@ -878,6 +920,40 @@ class FlexDirectory implements FlexDirectoryInterface
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$value = $this->mergeArrays($field[$property], $value); $value = $this->mergeArrays($field[$property], $value);
} }
$value = $not ? !$value : $value;
if ($property === 'ignore' && $value) {
Blueprint::addPropertyRecursive($field, 'validate', ['ignore' => true]);
} else {
$field[$property] = $value;
}
}
}
/**
* @param array $field
* @param string $property
* @param array $call
* @return void
*/
protected function dynamicAuthorizeField(array &$field, $property, array $call): void
{
$params = (array)$call['params'];
$object = $call['object'] ?? null;
$permission = array_shift($params);
$not = false;
if (str_starts_with($permission, '!')) {
$permission = substr($permission, 1);
$not = true;
} elseif (str_starts_with($permission, 'not ')) {
$permission = substr($permission, 4);
$not = true;
}
$permission = trim($permission);
if ($object) {
$value = $object->isAuthorized($permission) ?? false;
$field[$property] = $not ? !$value : $value; $field[$property] = $not ? !$value : $value;
} }
} }
@@ -275,6 +275,7 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
'unique_id' => $this->getUniqueId(), 'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(), 'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(), 'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId(),
'directory' => $this->getDirectory() 'directory' => $this->getDirectory()
]; ];
@@ -326,6 +326,7 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
'unique_id' => $this->getUniqueId(), 'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(), 'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(), 'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId(),
'object' => $this->getObject() 'object' => $this->getObject()
]; ];
@@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex;
use Grav\Common\Grav;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
use RuntimeException;
/**
* Interface IdentifierInterface
*
* @template T of FlexObjectInterface
* @extends Identifier<T>
*/
class FlexIdentifier extends Identifier
{
/** @var string */
private $keyField;
/** @var FlexObjectInterface|null */
private $object = null;
/**
* @param FlexObjectInterface $object
* @return FlexIdentifier<T>
*/
public static function createFromObject(FlexObjectInterface $object): FlexIdentifier
{
$instance = new static($object->getKey(), $object->getFlexType(), 'key');
$instance->setObject($object);
return $instance;
}
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
* @param string $keyField
*/
public function __construct(string $id, string $type, string $keyField = 'key')
{
parent::__construct($id, $type);
$this->keyField = $keyField;
}
/**
* @return T
*/
public function getObject(): ?FlexObjectInterface
{
if (!isset($this->object)) {
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
$this->object = $flex->getObject($this->getId(), $this->getType(), $this->keyField);
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(FlexObjectInterface $object): void
{
$type = $this->getType();
if ($type !== $object->getFlexType()) {
throw new RuntimeException(sprintf('Object has to be type %s, %s given', $type, $object->getFlexType()));
}
$this->object = $object;
}
}
@@ -162,6 +162,10 @@ class FlexIndex extends ObjectIndex implements FlexIndexInterface
*/ */
public function search(string $search, $properties = null, array $options = null) public function search(string $search, $properties = null, array $options = null)
{ {
$directory = $this->getFlexDirectory();
$properties = $directory->getSearchProperties($properties);
$options = $directory->getSearchOptions($options);
return $this->__call('search', [$search, $properties, $options]); return $this->__call('search', [$search, $properties, $options]);
} }
+3 -11
View File
@@ -287,17 +287,9 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function search(string $search, $properties = null, array $options = null): float public function search(string $search, $properties = null, array $options = null): float
{ {
$properties = (array)($properties ?? $this->getFlexDirectory()->getConfig('data.search.fields')); $directory = $this->getFlexDirectory();
if (!$properties) { $properties = $directory->getSearchProperties($properties);
$fields = $this->getFlexDirectory()->getConfig('admin.views.list.fields') ?? $this->getFlexDirectory()->getConfig('admin.list.fields', []); $options = $directory->getSearchOptions($options);
foreach ($fields as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
$weight = 0; $weight = 0;
foreach ($properties as $property) { foreach ($properties as $property) {
@@ -22,6 +22,9 @@ use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Form\FormFlashFile; use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Media\Interfaces\MediaObjectInterface;
use Grav\Framework\Media\MediaObject;
use Grav\Framework\Media\UploadedMediaObject;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException; use RuntimeException;
@@ -30,7 +33,6 @@ use function in_array;
use function is_array; use function is_array;
use function is_callable; use function is_callable;
use function is_int; use function is_int;
use function is_object;
use function is_string; use function is_string;
use function strpos; use function strpos;
@@ -297,6 +299,55 @@ trait FlexMediaTrait
]; ];
} }
/**
* @param string|null $field
* @param string $filename
* @param MediaObjectInterface|null $image
* @return MediaObject|UploadedMediaObject
*/
protected function buildMediaObject(?string $field, string $filename, MediaObjectInterface $image = null)
{
if (!$image) {
$media = $field ? $this->getMediaField($field) : null;
if ($media) {
$image = $media[$filename];
}
}
return new MediaObject($field, $filename, $image, $this);
}
/**
* @param string|null $field
* @return array
*/
protected function buildMediaList(?string $field): array
{
$names = $field ? (array)$this->getNestedProperty($field) : [];
$media = $field ? $this->getMediaField($field) : null;
if (null === $media) {
$media = $this->getMedia();
}
$list = [];
foreach ($names as $key => $val) {
$name = is_string($val) ? $val : $key;
$medium = $media[$name];
if ($medium) {
if ($medium->uploaded_file) {
$upload = $medium->uploaded_file;
$id = $upload instanceof FormFlashFile ? $upload->getId() : "{$field}-{$name}";
$list[] = new UploadedMediaObject($id, $field, $name, $upload);
} else {
$list[] = $this->buildMediaObject($field, $name, $medium);
}
}
}
return $list;
}
/** /**
* @param array $files * @param array $files
* @return void * @return void
@@ -375,7 +426,7 @@ trait FlexMediaTrait
$updated = false; $updated = false;
foreach ($this->getUpdatedMedia() as $filename => $upload) { foreach ($this->getUpdatedMedia() as $filename => $upload) {
if (is_array($upload)) { if (is_array($upload)) {
// Uses new format with [UploadedFileInterface, array]. /** @var array{UploadedFileInterface,array} $upload */
$settings = $upload[1]; $settings = $upload[1];
if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) { if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) {
$upload = $upload[0]; $upload = $upload[0];
@@ -388,6 +439,7 @@ trait FlexMediaTrait
$updated = true; $updated = true;
if ($medium) { if ($medium) {
$medium->uploaded = true; $medium->uploaded = true;
$medium->uploaded_file = $upload;
$media->add($filename, $medium); $media->add($filename, $medium);
} elseif (is_callable([$media, 'hide'])) { } elseif (is_callable([$media, 'hide'])) {
$media->hide($filename); $media->hide($filename);
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex\Traits;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Relationships\Relationships;
/**
* Trait FlexRelationshipsTrait
*/
trait FlexRelationshipsTrait
{
/** @var RelationshipsInterface|null */
private $_relationships = null;
/**
* @return Relationships
*/
public function getRelationships(): Relationships
{
if (!isset($this->_relationships)) {
$blueprint = $this->getBlueprint();
$options = $blueprint->get('config/relationships', []);
$parent = FlexIdentifier::createFromObject($this);
$this->_relationships = new Relationships($parent, $options);
}
return $this->_relationships;
}
/**
* @param string $name
* @return RelationshipInterface|null
*/
public function getRelationship(string $name): ?RelationshipInterface
{
return $this->getRelationships()[$name];
}
protected function resetRelationships(): void
{
$this->_relationships = null;
}
/**
* @param iterable $collection
* @return array
*/
protected function buildFlexIdentifierList(iterable $collection): array
{
$list = [];
foreach ($collection as $object) {
$list[] = FlexIdentifier::createFromObject($object);
}
return $list;
}
}
+17 -3
View File
@@ -31,6 +31,8 @@ class FormFlash implements FormFlashInterface
/** @var bool */ /** @var bool */
protected $exists; protected $exists;
/** @var string */ /** @var string */
protected $id;
/** @var string */
protected $sessionId; protected $sessionId;
/** @var string */ /** @var string */
protected $uniqueId; protected $uniqueId;
@@ -75,9 +77,12 @@ class FormFlash implements FormFlashInterface
}); });
} }
$this->sessionId = $config['session_id'] ?? 'no-session'; $this->id = $config['id'] ?? '';
$this->sessionId = $config['session_id'] ?? '';
$this->uniqueId = $config['unique_id'] ?? ''; $this->uniqueId = $config['unique_id'] ?? '';
$this->setUser($config['user'] ?? null);
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : ''); $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
@@ -133,6 +138,14 @@ class FormFlash implements FormFlashInterface
return $data; return $data;
} }
/**
* @inheritDoc
*/
public function getId(): string
{
return $this->id && $this->uniqueId ? $this->id . '/' . $this->uniqueId : '';
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -390,8 +403,8 @@ class FormFlash implements FormFlashInterface
*/ */
public function clearFiles() public function clearFiles()
{ {
foreach ($this->files as $field => $files) { foreach ($this->files as $files) {
foreach ($files as $name => $upload) { foreach ($files as $upload) {
$this->removeTmpFile($upload['tmp_name'] ?? ''); $this->removeTmpFile($upload['tmp_name'] ?? '');
} }
} }
@@ -406,6 +419,7 @@ class FormFlash implements FormFlashInterface
{ {
return [ return [
'form' => $this->formName, 'form' => $this->formName,
'id' => $this->getId(),
'unique_id' => $this->uniqueId, 'unique_id' => $this->uniqueId,
'url' => $this->url, 'url' => $this->url,
'user' => $this->user, 'user' => $this->user,
@@ -28,6 +28,8 @@ use function sprintf;
*/ */
class FormFlashFile implements UploadedFileInterface, JsonSerializable class FormFlashFile implements UploadedFileInterface, JsonSerializable
{ {
/** @var string */
private $id;
/** @var string */ /** @var string */
private $field; private $field;
/** @var bool */ /** @var bool */
@@ -45,6 +47,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
*/ */
public function __construct(string $field, array $upload, FormFlash $flash) public function __construct(string $field, array $upload, FormFlash $flash)
{ {
$this->id = $flash->getId() ?: $flash->getUniqueId();
$this->field = $field; $this->field = $field;
$this->upload = $upload; $this->upload = $upload;
$this->flash = $flash; $this->flash = $flash;
@@ -107,6 +110,11 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
} }
} }
public function getId(): string
{
return $this->id;
}
/** /**
* @return string * @return string
*/ */
@@ -222,6 +230,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
public function __debugInfo() public function __debugInfo()
{ {
return [ return [
'id:private' => $this->id,
'field:private' => $this->field, 'field:private' => $this->field,
'moved:private' => $this->moved, 'moved:private' => $this->moved,
'upload:private' => $this->upload, 'upload:private' => $this->upload,
@@ -22,6 +22,13 @@ interface FormFlashInterface extends \JsonSerializable
*/ */
public function __construct($config); public function __construct($config);
/**
* Get unique form flash id if set.
*
* @return string
*/
public function getId(): string;
/** /**
* Get session Id associated to this form instance. * Get session Id associated to this form instance.
* *
@@ -453,10 +453,10 @@ trait FormTrait
'session_id' => $this->getSessionId(), 'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(), 'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(), 'form_name' => $this->getName(),
'folder' => $this->getFlashFolder() 'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId()
]; ];
$this->flash = new FormFlash($config); $this->flash = new FormFlash($config);
$this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null); $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
} }
@@ -486,7 +486,8 @@ trait FormTrait
'session_id' => $this->getSessionId(), 'session_id' => $this->getSessionId(),
'unique_id' => $uniqueId, 'unique_id' => $uniqueId,
'form_name' => $name, 'form_name' => $name,
'folder' => $this->getFlashFolder() 'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId()
]; ];
$flash = new FormFlash($config); $flash = new FormFlash($config);
if ($flash->exists() && $flash->getFormName() === $name) { if ($flash->exists() && $flash->getFormName() === $name) {
@@ -614,6 +615,28 @@ trait FormTrait
return strpos($path, '!!') === false ? rtrim($path, '/') : null; return strpos($path, '!!') === false ? rtrim($path, '/') : null;
} }
/**
* @return string|null
*/
protected function getFlashId(): ?string
{
// Fill template token keys/value pairs.
$dataMap = [
'[FORM_NAME]' => $this->getName(),
'[SESSIONID]' => 'session',
'[USERNAME]' => '!!',
'[USERNAME_OR_SESSIONID]' => '!!',
'[ACCOUNT]' => 'account'
];
$flashLookupFolder = $this->getFlashLookupFolder();
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder);
// Make sure we only return valid paths.
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/** /**
* @return string * @return string
*/ */
@@ -9,9 +9,39 @@
namespace Grav\Framework\Media\Interfaces; namespace Grav\Framework\Media\Interfaces;
use Psr\Http\Message\UploadedFileInterface;
/** /**
* Class implements media object interface. * Class implements media object interface.
*
* @property UploadedFileInterface|null $uploaded_file
*/ */
interface MediaObjectInterface interface MediaObjectInterface
{ {
/**
* Returns an array containing the file metadata
*
* @return array
*/
public function getMeta();
/**
* Return URL to file.
*
* @param bool $reset
* @return string
*/
public function url($reset = true);
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $this->get('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string|null $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function get($name, $default = null, $separator = null);
} }
@@ -0,0 +1,150 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
/**
* Interface IdentifierInterface
*
* @template T of MediaObjectInterface
* @extends Identifier<T>
*/
class MediaIdentifier extends Identifier
{
/** @var MediaObjectInterface|null */
private $object = null;
/**
* @param MediaObjectInterface $object
* @return MediaIdentifier<T>
*/
public static function createFromObject(MediaObjectInterface $object): MediaIdentifier
{
$instance = new static($object->getId());
$instance->setObject($object);
return $instance;
}
/**
* @param string $id
*/
public function __construct(string $id)
{
parent::__construct($id, 'media');
}
/**
* @return T
*/
public function getObject(): ?MediaObjectInterface
{
if (!isset($this->object)) {
$type = $this->getType();
$id = $this->getId();
$parts = explode('/', $id);
if ($type === 'media' && str_starts_with($id, 'uploads/')) {
array_shift($parts);
[, $folder, $uniqueId, $field, $filename] = $this->findFlash($parts);
$flash = $this->getFlash($folder, $uniqueId);
if ($flash->exists()) {
$uploadedFile = $flash->getFilesByField($field)[$filename] ?? null;
$this->object = UploadedMediaObject::createFromFlash($flash, $field, $filename, $uploadedFile);
}
} else {
$type = array_shift($parts);
$key = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
$flexObject = $this->getFlexObject($type, $key);
if ($flexObject && method_exists($flexObject, 'getMediaField') && method_exists($flexObject, 'getMedia')) {
$media = $field !== 'media' ? $flexObject->getMediaField($field) : $flexObject->getMedia();
$image = null;
if ($media) {
$image = $media[$filename];
}
$this->object = new MediaObject($field, $filename, $image, $flexObject);
}
}
if (!isset($this->object)) {
throw new \RuntimeException(sprintf('Object not found for identifier {type: "%s", id: "%s"}', $type, $id));
}
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(MediaObjectInterface $object): void
{
$type = $this->getType();
$objectType = $object->getType();
if ($type !== $objectType) {
throw new \RuntimeException(sprintf('Object has to be type %s, %s given', $type, $objectType));
}
$this->object = $object;
}
protected function findFlash(array $parts): ?array
{
$type = array_shift($parts);
if ($type === 'account') {
/** @var UserInterface|null $user */
$user = Grav::instance()['user'] ?? null;
$folder = $user->getMediaFolder();
} else {
$folder = 'tmp://';
}
if (!$folder) {
return null;
}
do {
$part = array_shift($parts);
$folder .= "/{$part}";
} while (!str_starts_with($part, 'flex-'));
$uniqueId = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
return [$type, $folder, $uniqueId, $field, $filename];
}
protected function getFlash(string $folder, string $uniqueId): FlexFormFlash
{
$config = [
'unique_id' => $uniqueId,
'folder' => $folder
];
return new FlexFormFlash($config);
}
protected function getFlexObject(string $type, string $key): ?FlexObjectInterface
{
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
return $flex->getObject($key, $type);
}
}
@@ -0,0 +1,215 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Media\Interfaces\MediaObjectInterface as GravMediaObjectInterface;
use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Throwable;
/**
* Class MediaObject
*/
class MediaObject implements MediaObjectInterface
{
/** @var string */
static public $placeholderImage = 'image://media/thumb.png';
/** @var FlexObjectInterface */
public $object;
/** @var GravMediaObjectInterface|null */
public $media;
/** @var string|null */
private $field;
/** @var string */
private $filename;
/**
* MediaObject constructor.
* @param string|null $field
* @param string $filename
* @param GravMediaObjectInterface|null $media
* @param FlexObjectInterface $object
*/
public function __construct(?string $field, string $filename, ?GravMediaObjectInterface $media, FlexObjectInterface $object)
{
$this->field = $field;
$this->filename = $filename;
$this->media = $media;
$this->object = $object;
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$field = $this->field;
$object = $this->object;
$path = $field ? "/{$field}/" : '/media/';
return $object->getType() . '/' . $object->getKey() . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
return $this->media !== null;
}
/**
* @return array
*/
public function getMeta(): array
{
if (!isset($this->media)) {
return [];
}
return $this->media->getMeta();
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
if (!isset($this->media)) {
return null;
}
return $this->media->get($field);
}
/**
* @return string
*/
public function getUrl(): string
{
if (!isset($this->media)) {
return '';
}
return $this->media->url();
}
/**
* Create media response.
*
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): ResponseInterface
{
if (!isset($this->media)) {
return $this->create404Response($actions);
}
$media = $this->media;
if ($actions) {
$media = $this->processMediaActions($media, $actions);
}
// FIXME: This only works for images
if (!$media instanceof ImageMedium) {
throw new \RuntimeException('Not Implemented', 500);
}
$filename = $media->path(false);
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => $media->get('mime'),
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(200, $headers, $body);
}
/**
* Process media actions
*
* @param GravMediaObjectInterface $medium
* @param array $actions
* @return GravMediaObjectInterface
*/
protected function processMediaActions(GravMediaObjectInterface $medium, array $actions): GravMediaObjectInterface
{
// loop through actions for the image and call them
foreach ($actions as $method => $params) {
$matches = [];
if (preg_match('/\[(.*)]/', $params, $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $params);
}
try {
$medium->{$method}(...$args);
} catch (Throwable $e) {
// Ignore all errors for now and just skip the action.
}
}
return $medium;
}
/**
* @param array $actions
* @return Response
*/
protected function create404Response(array $actions): Response
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}
@@ -0,0 +1,172 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* Class UploadedMediaObject
*/
class UploadedMediaObject implements MediaObjectInterface
{
/** @var string */
static public $placeholderImage = 'image://media/thumb.png';
/** @var FormFlashInterface */
public $object;
/** @var string */
private $id;
/** @var string|null */
private $field;
/** @var string */
private $filename;
/** @var array */
private $meta;
/** @var UploadedFileInterface|null */
private $uploadedFile;
/**
* @param FlexFormFlash $flash
* @param string|null $field
* @param string $filename
* @param UploadedFileInterface|null $uploadedFile
* @return static
*/
public static function createFromFlash(FlexFormFlash $flash, ?string $field, string $filename, ?UploadedFileInterface $uploadedFile = null)
{
$id = $flash->getId();
return new static($id, $field, $filename, $uploadedFile);
}
/**
* @param string $id
* @param string|null $field
* @param string $filename
* @param UploadedFileInterface|null $uploadedFile
*/
public function __construct(string $id, ?string $field, string $filename, ?UploadedFileInterface $uploadedFile = null)
{
$this->id = $id;
$this->field = $field;
$this->filename = $filename;
$this->uploadedFile = $uploadedFile;
if ($uploadedFile) {
$this->meta = [
'filename' => $uploadedFile->getClientFilename(),
'mime' => $uploadedFile->getClientMediaType(),
'size' => $uploadedFile->getSize()
];
} else {
$this->meta = [];
}
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$id = $this->id;
$field = $this->field;
$path = $field ? "/{$field}/" : '';
return 'uploads/' . $id . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
//return $this->uploadedFile !== null;
return false;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
return $this->meta[$field] ?? null;
}
/**
* @return string
*/
public function getUrl(): string
{
return '';
}
/**
* @return UploadedFileInterface|null
*/
public function getUploadedFile(): ?UploadedFileInterface
{
return $this->uploadedFile;
}
/**
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): ResponseInterface
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}
@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Object\Identifiers;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface IdentifierInterface
*
* @template T of object
*/
class Identifier implements IdentifierInterface
{
/** @var string */
private $id;
/** @var string */
private $type;
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
*/
public function __construct(string $id, string $type)
{
$this->id = $id;
$this->type = $type;
}
/**
* @return string
* @phpstan-pure
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->type,
'id' => $this->id
];
}
/**
* @return array
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}
@@ -0,0 +1,217 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use RuntimeException;
use function count;
/**
* Class Relationships
*
* @template T of \Grav\Framework\Contracts\Object\IdentifierInterface
* @template P of \Grav\Framework\Contracts\Object\IdentifierInterface
* @implements RelationshipsInterface<T,P>
*/
class Relationships implements RelationshipsInterface
{
/** @var P */
protected $parent;
/** @var array */
protected $options;
/** @var RelationshipInterface<T,P>[] */
protected $relationships;
/**
* Relationships constructor.
* @param P $parent
* @param array $options
*/
public function __construct(IdentifierInterface $parent, array $options)
{
$this->parent = $parent;
$this->options = $options;
$this->relationships = [];
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return !empty($this->getModified());
}
/**
* @return RelationshipInterface<T,P>[]
* @phpstan-pure
*/
public function getModified(): array
{
$list = [];
foreach ($this->relationships as $name => $relationship) {
if ($relationship->isModified()) {
$list[$name] = $relationship;
}
}
return $list;
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->options);
}
/**
* @param string $offset
* @return bool
* @phpstan-pure
*/
public function offsetExists($offset): bool
{
return isset($this->options[$offset]);
}
/**
* @param string $offset
* @return RelationshipInterface<T,P>|null
*/
public function offsetGet($offset): ?RelationshipInterface
{
if (!isset($this->relationships[$offset])) {
$options = $this->options[$offset] ?? null;
if (null === $options) {
return null;
}
$this->relationships[$offset] = $this->createRelationship($offset, $options);
}
return $this->relationships[$offset];
}
/**
* @param string $offset
* @param mixed $value
* @return never-return
*/
public function offsetSet($offset, $value)
{
throw new RuntimeException('Setting relationship is not supported', 500);
}
/**
* @param string $offset
* @return never-return
*/
public function offsetUnset($offset)
{
throw new RuntimeException('Removing relationship is not allowed', 500);
}
/**
* @return RelationshipInterface<T,P>|null
*/
public function current(): ?RelationshipInterface
{
$name = key($this->options);
if ($name === null) {
return null;
}
return $this->offsetGet($name);
}
/**
* @return string
* @phpstan-pure
*/
public function key(): string
{
return key($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function next(): void
{
next($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function rewind(): void
{
reset($this->options);
}
/**
* @return bool
* @phpstan-pure
*/
public function valid(): bool
{
return key($this->options) !== null;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this as $name => $relationship) {
$list[$name] = $relationship->jsonSerialize();
}
return $list;
}
/**
* @param string $name
* @param array $options
* @return ToOneRelationship|ToManyRelationship
*/
private function createRelationship(string $name, array $options): RelationshipInterface
{
$data = null;
$parent = $this->parent;
if ($parent instanceof FlexIdentifier) {
$object = $parent->getObject();
if (!method_exists($object, 'initRelationship')) {
throw new RuntimeException(sprintf('Bad relationship %s', $name), 500);
}
$data = $object->initRelationship($name);
}
$cardinality = $options['cardinality'] ?? '';
switch ($cardinality) {
case 'to-one':
$relationship = new ToOneRelationship($parent, $name, $options, $data);
break;
case 'to-many':
$relationship = new ToManyRelationship($parent, $name, $options, $data ?? []);
break;
default:
throw new RuntimeException(sprintf('Bad relationship cardinality %s', $cardinality), 500);
}
return $relationship;
}
}
@@ -0,0 +1,259 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToManyRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function count;
use function is_callable;
/**
* Class ToManyRelationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-implements ToManyRelationshipInterface<T,P>
*/
class ToManyRelationship implements ToManyRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
/** @var IdentifierInterface[] */
protected $identifiers = [];
/**
* ToManyRelationship constructor.
* @param string $name
* @param IdentifierInterface $parent
* @param iterable<IdentifierInterface> $identifiers
*/
public function __construct(IdentifierInterface $parent, string $name, array $options, iterable $identifiers = [])
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->addIdentifiers($identifiers);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-many';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->identifiers);
}
/**
* @return array
*/
public function fetch(): array
{
$list = [];
foreach ($this->identifiers as $identifier) {
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
$list[] = $identifier;
}
return $list;
}
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param positive-int $pos
* @return IdentifierInterface|null
*/
public function getNthIdentifier(int $pos): ?IdentifierInterface
{
$items = array_keys($this->identifiers);
$key = $items[$pos - 1] ?? null;
if (null === $key) {
return null;
}
return $this->identifiers[$key] ?? null;
}
/**
* @param string $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface
{
if (null === $type) {
$type = $this->getType();
}
if ($type === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$key = "{$type}/{$id}";
return $this->identifiers[$key] ?? null;
}
/**
* @param string $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
return $this->addIdentifiers([$identifier]);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
return !$identifier || $this->removeIdentifiers([$identifier]);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
$this->identifiers[$key] = $this->checkIdentifier($identifier);
$this->modified = true;
}
return true;
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool
{
$this->identifiers = [];
$this->modified = true;
return $this->addIdentifiers($identifiers);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
unset($this->identifiers[$key]);
$this->modified = true;
}
return true;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator($this->identifiers);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this->getIterator() as $item) {
$list[] = $item->jsonSerialize();
}
return $list;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifiers' => $this->identifiers,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifiers = $data['identifiers'];
}
}
@@ -0,0 +1,207 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function is_callable;
/**
* Class ToOneRelationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-implements ToOneRelationshipInterface<T,P>
*/
class ToOneRelationship implements ToOneRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
/** @var IdentifierInterface|null */
protected $identifier = null;
public function __construct(IdentifierInterface $parent, string $name, array $options, IdentifierInterface $identifier = null)
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->replaceIdentifier($identifier);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-one';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return $this->identifier ? 1 : 0;
}
/**
* @return object|null
*/
public function fetch(): ?object
{
$identifier = $this->identifier;
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id = null, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param string|null $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface
{
if ($id && $this->getType() === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$identifier = $this->identifier ?? null;
if (null === $identifier || ($type && $type !== $identifier->getType()) || ($id && $id !== $identifier->getId())) {
return null;
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id = null, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
$this->identifier = $this->checkIdentifier($identifier);
$this->modified = true;
return true;
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool
{
if ($identifier === null) {
$this->identifier = null;
$this->modified = true;
return true;
}
return $this->addIdentifier($identifier);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
if (null === $identifier || $this->has($identifier->getId(), $identifier->getType())) {
$this->identifier = null;
$this->modified = true;
return true;
}
return false;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator((array)$this->identifier);
}
/**
* @return array|null
*/
public function jsonSerialize(): ?array
{
return $this->identifier ? $this->identifier->jsonSerialize() : null;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifier' => $this->identifier,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifier = $data['identifier'];
}
}
@@ -0,0 +1,128 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships\Traits;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Media\MediaIdentifier;
use Grav\Framework\Object\Identifiers\Identifier;
use RuntimeException;
use function get_class;
/**
* Trait RelationshipTrait
*
* @template T of object
*/
trait RelationshipTrait
{
/** @var IdentifierInterface */
protected $parent;
/** @var string */
protected $name;
/** @var string */
protected $type;
/** @var array */
protected $options;
/** @var bool */
protected $modified = false;
/**
* @return string
* @phpstan-pure
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return $this->modified;
}
/**
* @return IdentifierInterface
* @phpstan-pure
*/
public function getParent(): IdentifierInterface
{
return $this->parent;
}
/**
* @param IdentifierInterface $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool
{
return $this->getIdentifier($identifier->getId(), $identifier->getType()) !== null;
}
/**
* @return int
* @phpstan-pure
*/
abstract public function count(): int;
/**
* @return void
* @phpstan-pure
*/
public function check(): void
{
$min = $this->options['min'] ?? 0;
$max = $this->options['max'] ?? 0;
if ($min || $max) {
$count = $this->count();
if ($min && $count < $min) {
throw new RuntimeException(sprintf('%s relationship has too few objects in it', $this->name));
}
if ($max && $count > $max) {
throw new RuntimeException(sprintf('%s relationship has too many objects in it', $this->name));
}
}
}
/**
* @param IdentifierInterface $identifier
* @return IdentifierInterface
*/
private function checkIdentifier(IdentifierInterface $identifier): IdentifierInterface
{
if ($this->type !== $identifier->getType()) {
throw new RuntimeException(sprintf('Bad identifier type %s', $identifier->getType()));
}
if (get_class($identifier) !== Identifier::class) {
return $identifier;
}
if ($this->type === 'media') {
return new MediaIdentifier($identifier->getId());
}
return new FlexIdentifier($identifier->getId(), $identifier->getType());
}
private function parseOptions(array $options): void
{
$this->type = $options['type'];
$this->options = $options;
}
}
@@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @package Grav\Framework\RequestHandler * @package Grav\Framework\RequestHandler
@@ -7,8 +7,6 @@
* @license MIT License; see LICENSE file for details. * @license MIT License; see LICENSE file for details.
*/ */
declare(strict_types=1);
namespace Grav\Framework\RequestHandler\Middlewares; namespace Grav\Framework\RequestHandler\Middlewares;
use Grav\Common\Data\ValidationException; use Grav\Common\Data\ValidationException;
@@ -0,0 +1,123 @@
<?php declare(strict_types=1);
/**
* @package Grav\Framework\RequestHandler
*
* @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\RequestHandler\Middlewares;
use Grav\Framework\Psr7\UploadedFile;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function array_slice;
use function count;
use function in_array;
use function is_array;
use function strlen;
/**
* Multipart request support for PUT and PATCH.
*/
class MultipartRequestSupport implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$contentType = $request->getHeaderLine('content-type');
$method = $request->getMethod();
if (!str_starts_with($contentType, 'multipart/form-data') || !in_array($method, ['PUT', 'PATH'], true)) {
return $handler->handle($request);
}
$boundary = explode('; boundary=', $contentType, 2)[1] ?? '';
$parts = explode("--{$boundary}", $request->getBody()->getContents());
$parts = array_slice($parts, 1, count($parts) - 2);
$params = [];
$files = [];
foreach ($parts as $part) {
$this->processPart($params, $files, $part);
}
return $handler->handle($request->withParsedBody($params)->withUploadedFiles($files));
}
/**
* @param array $params
* @param array $files
* @param string $part
* @return void
*/
protected function processPart(array &$params, array &$files, string $part): void
{
$part = ltrim($part, "\r\n");
[$rawHeaders, $body] = explode("\r\n\r\n", $part, 2);
// Parse headers.
$rawHeaders = explode("\r\n", $rawHeaders);
$headers = array_reduce(
$rawHeaders,
static function (array $headers, $header) {
[$name, $value] = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
return $headers;
},
[]
);
if (!isset($headers['content-disposition'])) {
return;
}
// Parse content disposition header.
$contentDisposition = $headers['content-disposition'];
preg_match('/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $contentDisposition, $matches);
$name = $matches[2];
$filename = $matches[4] ?? null;
if ($filename !== null) {
$stream = Stream::create($body);
$this->addFile($files, $name, new UploadedFile($stream, strlen($body), UPLOAD_ERR_OK, $filename, $headers['content-type'] ?? null));
} elseif (strpos($contentDisposition, 'filename') !== false) {
// Not uploaded file.
$stream = Stream::create('');
$this->addFile($files, $name, new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE));
} else {
// Regular field.
$params[$name] = substr($body, 0, -2);
}
}
/**
* @param array $files
* @param string $name
* @param UploadedFileInterface $file
* @return void
*/
protected function addFile(array &$files, string $name, UploadedFileInterface $file): void
{
if (strpos($name, '[]') === strlen($name) - 2) {
$name = substr($name, 0, -2);
if (isset($files[$name]) && is_array($files[$name])) {
$files[$name][] = $file;
} else {
$files[$name] = [$file];
}
} else {
$files[$name] = $file;
}
}
}
@@ -28,10 +28,10 @@ trait RequestHandlerTrait
protected $middleware; protected $middleware;
/** @var callable */ /** @var callable */
private $handler; protected $handler;
/** @var ContainerInterface|null */ /** @var ContainerInterface|null */
private $container; protected $container;
/** /**
* {@inheritdoc} * {@inheritdoc}