maj
This commit is contained in:
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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, ':');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>');
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user