updated core to 1.7.15
This commit is contained in:
File diff suppressed because it is too large
Load Diff
258
system/src/Grav/Common/Assets/BaseAsset.php
Normal file
258
system/src/Grav/Common/Assets/BaseAsset.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Assets\Traits\AssetUtilsTrait;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Object\PropertyObject;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Class BaseAsset
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
abstract class BaseAsset extends PropertyObject
|
||||
{
|
||||
use AssetUtilsTrait;
|
||||
|
||||
protected const CSS_ASSET = true;
|
||||
protected const JS_ASSET = false;
|
||||
|
||||
/** @var string|false */
|
||||
protected $asset;
|
||||
/** @var string */
|
||||
protected $asset_type;
|
||||
/** @var int */
|
||||
protected $order;
|
||||
/** @var string */
|
||||
protected $group;
|
||||
/** @var string */
|
||||
protected $position;
|
||||
/** @var int */
|
||||
protected $priority;
|
||||
/** @var array */
|
||||
protected $attributes = [];
|
||||
|
||||
/** @var string */
|
||||
protected $timestamp;
|
||||
/** @var int|false */
|
||||
protected $modified;
|
||||
/** @var bool */
|
||||
protected $remote;
|
||||
/** @var string */
|
||||
protected $query = '';
|
||||
|
||||
// Private Bits
|
||||
/** @var bool */
|
||||
private $css_rewrite = false;
|
||||
/** @var bool */
|
||||
private $css_minify = false;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract function render();
|
||||
|
||||
/**
|
||||
* BaseAsset constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_config = [
|
||||
'group' => 'head',
|
||||
'position' => 'pipeline',
|
||||
'priority' => 10,
|
||||
'modified' => null,
|
||||
'asset' => null
|
||||
];
|
||||
|
||||
// Merge base defaults
|
||||
$elements = array_merge($base_config, $elements);
|
||||
|
||||
parent::__construct($elements, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false $asset
|
||||
* @param array $options
|
||||
* @return $this|false
|
||||
*/
|
||||
public function init($asset, $options)
|
||||
{
|
||||
$config = Grav::instance()['config'];
|
||||
$uri = Grav::instance()['uri'];
|
||||
|
||||
// set attributes
|
||||
foreach ($options as $key => $value) {
|
||||
if ($this->hasProperty($key)) {
|
||||
$this->setProperty($key, $value);
|
||||
} else {
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Force priority to be an int
|
||||
$this->priority = (int) $this->priority;
|
||||
|
||||
// Do some special stuff for CSS/JS (not inline)
|
||||
if (!Utils::startsWith($this->getType(), 'inline')) {
|
||||
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
|
||||
$this->remote = static::isRemoteLink($asset);
|
||||
|
||||
// Move this to render?
|
||||
if (!$this->remote) {
|
||||
$asset_parts = parse_url($asset);
|
||||
if (isset($asset_parts['query'])) {
|
||||
$this->query = $asset_parts['query'];
|
||||
unset($asset_parts['query']);
|
||||
$asset = Uri::buildUrl($asset_parts);
|
||||
}
|
||||
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if ($locator->isStream($asset)) {
|
||||
$path = $locator->findResource($asset, true);
|
||||
} else {
|
||||
$path = GRAV_WEBROOT . $asset;
|
||||
}
|
||||
|
||||
// If local file is missing return
|
||||
if ($path === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file = new SplFileInfo($path);
|
||||
|
||||
$asset = $this->buildLocalLink($file->getPathname());
|
||||
|
||||
$this->modified = $file->isFile() ? $file->getMTime() : false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->asset = $asset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
*/
|
||||
public function getAsset()
|
||||
{
|
||||
return $this->asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getRemote()
|
||||
{
|
||||
return $this->remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $position
|
||||
* @return $this
|
||||
*/
|
||||
public function setPosition($position)
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive asset location and return the SRI integrity hash
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public static function integrityHash($input)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
$assetsConfig = $grav['config']->get('system.assets');
|
||||
|
||||
if ( !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri'] )
|
||||
{
|
||||
$dataToHash = file_get_contents( GRAV_WEBROOT . $input);
|
||||
|
||||
$hash = hash('sha256', $dataToHash, true);
|
||||
$hash_base64 = base64_encode($hash);
|
||||
return ' integrity="sha256-' . $hash_base64 . '"';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the last modification time of asset
|
||||
*
|
||||
* @param string $asset the asset string reference
|
||||
*
|
||||
* @return string the last modifcation time or false on error
|
||||
*/
|
||||
// protected function getLastModificationTime($asset)
|
||||
// {
|
||||
// $file = GRAV_WEBROOT . $asset;
|
||||
// if (Grav::instance()['locator']->isStream($asset)) {
|
||||
// $file = $this->buildLocalLink($asset, true);
|
||||
// }
|
||||
//
|
||||
// return file_exists($file) ? filemtime($file) : false;
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
* Build local links including grav asset shortcodes
|
||||
*
|
||||
* @param string $asset the asset string reference
|
||||
*
|
||||
* @return string|false the final link url to the asset
|
||||
*/
|
||||
protected function buildLocalLink($asset)
|
||||
{
|
||||
if ($asset) {
|
||||
return $this->base_url . ltrim(Utils::replaceFirstOccurrence(GRAV_WEBROOT, '', $asset), '/');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements JsonSerializable interface.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return ['type' => $this->getType(), 'elements' => $this->getElements()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for AssetUtilsTrait method
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $dir
|
||||
* @param bool $local
|
||||
* @return string
|
||||
*/
|
||||
protected function cssRewrite($file, $dir, $local)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
52
system/src/Grav/Common/Assets/Css.php
Normal file
52
system/src/Grav/Common/Assets/Css.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Class Css
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
class Css extends BaseAsset
|
||||
{
|
||||
/**
|
||||
* Css constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_options = [
|
||||
'asset_type' => 'css',
|
||||
'attributes' => [
|
||||
'type' => 'text/css',
|
||||
'rel' => 'stylesheet'
|
||||
]
|
||||
];
|
||||
|
||||
$merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
|
||||
|
||||
parent::__construct($merged_attributes, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
|
||||
$buffer = $this->gatherLinks([$this], self::CSS_ASSET);
|
||||
return "<style>\n" . trim($buffer) . "\n</style>\n";
|
||||
}
|
||||
|
||||
return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . ">\n";
|
||||
}
|
||||
}
|
||||
44
system/src/Grav/Common/Assets/InlineCss.php
Normal file
44
system/src/Grav/Common/Assets/InlineCss.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Class InlineCss
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
class InlineCss extends BaseAsset
|
||||
{
|
||||
/**
|
||||
* InlineCss constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_options = [
|
||||
'asset_type' => 'css',
|
||||
'position' => 'after'
|
||||
];
|
||||
|
||||
$merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
|
||||
|
||||
parent::__construct($merged_attributes, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return '<style' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</style>\n";
|
||||
}
|
||||
}
|
||||
44
system/src/Grav/Common/Assets/InlineJs.php
Normal file
44
system/src/Grav/Common/Assets/InlineJs.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Class InlineJs
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
class InlineJs extends BaseAsset
|
||||
{
|
||||
/**
|
||||
* InlineJs constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_options = [
|
||||
'asset_type' => 'js',
|
||||
'position' => 'after'
|
||||
];
|
||||
|
||||
$merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
|
||||
|
||||
parent::__construct($merged_attributes, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return '<script' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</script>\n";
|
||||
}
|
||||
}
|
||||
48
system/src/Grav/Common/Assets/Js.php
Normal file
48
system/src/Grav/Common/Assets/Js.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Class Js
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
class Js extends BaseAsset
|
||||
{
|
||||
/**
|
||||
* Js constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_options = [
|
||||
'asset_type' => 'js',
|
||||
];
|
||||
|
||||
$merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
|
||||
|
||||
parent::__construct($merged_attributes, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
|
||||
$buffer = $this->gatherLinks([$this], self::JS_ASSET);
|
||||
return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
|
||||
}
|
||||
|
||||
return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . $this->integrityHash($this->asset) . "></script>\n";
|
||||
}
|
||||
}
|
||||
280
system/src/Grav/Common/Assets/Pipeline.php
Normal file
280
system/src/Grav/Common/Assets/Pipeline.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Assets\BaseAsset;
|
||||
use Grav\Common\Assets\Traits\AssetUtilsTrait;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Object\PropertyObject;
|
||||
use MatthiasMullie\Minify\CSS;
|
||||
use MatthiasMullie\Minify\JS;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Class Pipeline
|
||||
* @package Grav\Common\Assets
|
||||
*/
|
||||
class Pipeline extends PropertyObject
|
||||
{
|
||||
use AssetUtilsTrait;
|
||||
|
||||
protected const CSS_ASSET = true;
|
||||
protected const JS_ASSET = false;
|
||||
|
||||
/** @const Regex to match CSS urls */
|
||||
protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
|
||||
|
||||
/** @const Regex to match CSS sourcemap comments */
|
||||
protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
|
||||
|
||||
protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
|
||||
|
||||
// Following variables come from the configuration:
|
||||
/** @var bool */
|
||||
protected $css_minify = false;
|
||||
/** @var bool */
|
||||
protected $css_minify_windows = false;
|
||||
/** @var bool */
|
||||
protected $css_rewrite = false;
|
||||
/** @var bool */
|
||||
protected $css_pipeline_include_externals = true;
|
||||
/** @var bool */
|
||||
protected $js_minify = false;
|
||||
/** @var bool */
|
||||
protected $js_minify_windows = false;
|
||||
/** @var bool */
|
||||
protected $js_pipeline_include_externals = true;
|
||||
|
||||
/** @var string */
|
||||
protected $assets_dir;
|
||||
/** @var string */
|
||||
protected $assets_url;
|
||||
/** @var string */
|
||||
protected $timestamp;
|
||||
/** @var array */
|
||||
protected $attributes;
|
||||
/** @var string */
|
||||
protected $query = '';
|
||||
/** @var string */
|
||||
protected $asset;
|
||||
|
||||
/**
|
||||
* Pipeline constructor.
|
||||
* @param array $elements
|
||||
* @param string|null $key
|
||||
*/
|
||||
public function __construct(array $elements = [], ?string $key = null)
|
||||
{
|
||||
parent::__construct($elements, $key);
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = Grav::instance()['uri'];
|
||||
|
||||
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
|
||||
$this->assets_dir = $locator->findResource('asset://') . DS;
|
||||
$this->assets_url = $locator->findResource('asset://', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify and concatenate CSS
|
||||
*
|
||||
* @param array $assets
|
||||
* @param string $group
|
||||
* @param array $attributes
|
||||
* @return bool|string URL or generated content if available, else false
|
||||
*/
|
||||
public function renderCss($assets, $group, $attributes = [])
|
||||
{
|
||||
// temporary list of assets to pipeline
|
||||
$inline_group = false;
|
||||
|
||||
if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
|
||||
$inline_group = true;
|
||||
unset($attributes['loading']);
|
||||
}
|
||||
|
||||
// Store Attributes
|
||||
$this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes);
|
||||
|
||||
// Compute uid based on assets and timestamp
|
||||
$json_assets = json_encode($assets);
|
||||
$uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
|
||||
$file = $uid . '.css';
|
||||
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
|
||||
|
||||
$buffer = null;
|
||||
|
||||
if (file_exists($this->assets_dir . $file)) {
|
||||
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
|
||||
} else {
|
||||
//if nothing found get out of here!
|
||||
if (empty($assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Concatenate files
|
||||
$buffer = $this->gatherLinks($assets, self::CSS_ASSET);
|
||||
|
||||
// Minify if required
|
||||
if ($this->shouldMinify('css')) {
|
||||
$minifier = new CSS();
|
||||
$minifier->add($buffer);
|
||||
$buffer = $minifier->minify();
|
||||
}
|
||||
|
||||
// Write file
|
||||
if (trim($buffer) !== '') {
|
||||
file_put_contents($this->assets_dir . $file, $buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if ($inline_group) {
|
||||
$output = "<style>\n" . $buffer . "\n</style>\n";
|
||||
} else {
|
||||
$this->asset = $relative_path;
|
||||
$output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify and concatenate JS files.
|
||||
*
|
||||
* @param array $assets
|
||||
* @param string $group
|
||||
* @param array $attributes
|
||||
* @return bool|string URL or generated content if available, else false
|
||||
*/
|
||||
public function renderJs($assets, $group, $attributes = [])
|
||||
{
|
||||
// temporary list of assets to pipeline
|
||||
$inline_group = false;
|
||||
|
||||
if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
|
||||
$inline_group = true;
|
||||
unset($attributes['loading']);
|
||||
}
|
||||
|
||||
// Store Attributes
|
||||
$this->attributes = $attributes;
|
||||
|
||||
// Compute uid based on assets and timestamp
|
||||
$json_assets = json_encode($assets);
|
||||
$uid = md5($json_assets . $this->js_minify . $group);
|
||||
$file = $uid . '.js';
|
||||
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
|
||||
|
||||
$buffer = null;
|
||||
|
||||
if (file_exists($this->assets_dir . $file)) {
|
||||
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
|
||||
} else {
|
||||
//if nothing found get out of here!
|
||||
if (empty($assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Concatenate files
|
||||
$buffer = $this->gatherLinks($assets, self::JS_ASSET);
|
||||
|
||||
// Minify if required
|
||||
if ($this->shouldMinify('js')) {
|
||||
$minifier = new JS();
|
||||
$minifier->add($buffer);
|
||||
$buffer = $minifier->minify();
|
||||
}
|
||||
|
||||
// Write file
|
||||
if (trim($buffer) !== '') {
|
||||
file_put_contents($this->assets_dir . $file, $buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if ($inline_group) {
|
||||
$output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
|
||||
} else {
|
||||
$this->asset = $relative_path;
|
||||
$output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . "></script>\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds relative CSS urls() and rewrites the URL with an absolute one
|
||||
*
|
||||
* @param string $file the css source file
|
||||
* @param string $dir , $local relative path to the css file
|
||||
* @param bool $local is this a local or remote asset
|
||||
* @return string
|
||||
*/
|
||||
protected function cssRewrite($file, $dir, $local)
|
||||
{
|
||||
// Strip any sourcemap comments
|
||||
$file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
|
||||
|
||||
// Find any css url() elements, grab the URLs and calculate an absolute path
|
||||
// Then replace the old url with the new one
|
||||
$file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) {
|
||||
|
||||
$old_url = $matches[2];
|
||||
|
||||
// Ensure link is not rooted to web server, a data URL, or to a remote host
|
||||
if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
// clean leading /
|
||||
$old_url = Utils::normalizePath($dir . '/' . $old_url);
|
||||
if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
|
||||
$old_url = ltrim($old_url, '/');
|
||||
}
|
||||
|
||||
$new_url = ($local ? $this->base_url: '') . $old_url;
|
||||
|
||||
return str_replace($matches[2], $new_url, $matches[0]);
|
||||
}, $file);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldMinify($type = 'css')
|
||||
{
|
||||
$check = $type . '_minify';
|
||||
$win_check = $type . '_minify_windows';
|
||||
|
||||
$minify = (bool) $this->$check;
|
||||
|
||||
// If this is a Windows server, and minify_windows is false (default value) skip the
|
||||
// minification process because it will cause Apache to die/crash due to insufficient
|
||||
// ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
|
||||
if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) {
|
||||
$minify = false;
|
||||
}
|
||||
|
||||
return $minify;
|
||||
}
|
||||
}
|
||||
208
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
Normal file
208
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use Closure;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use function dirname;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Trait AssetUtilsTrait
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*/
|
||||
trait AssetUtilsTrait
|
||||
{
|
||||
/**
|
||||
* @var Closure|null
|
||||
*
|
||||
* Closure used by the pipeline to fetch assets.
|
||||
*
|
||||
* Useful when file_get_contents() function is not available in your PHP
|
||||
* installation or when you want to apply any kind of preprocessing to
|
||||
* your assets before they get pipelined.
|
||||
*
|
||||
* The closure will receive as the only parameter a string with the path/URL of the asset and
|
||||
* it should return the content of the asset file as a string.
|
||||
*/
|
||||
protected $fetch_command;
|
||||
|
||||
/** @var string */
|
||||
protected $base_url;
|
||||
|
||||
/**
|
||||
* Determine whether a link is local or remote.
|
||||
* Understands both "http://" and "https://" as well as protocol agnostic links "//"
|
||||
*
|
||||
* @param string $link
|
||||
* @return bool
|
||||
*/
|
||||
public static function isRemoteLink($link)
|
||||
{
|
||||
$base = Grav::instance()['uri']->rootUrl(true);
|
||||
|
||||
// Sanity check for local URLs with absolute URL's enabled
|
||||
if (Utils::startsWith($link, $base)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and concatenate the content of several links.
|
||||
*
|
||||
* @param array $assets
|
||||
* @param bool $css
|
||||
* @return string
|
||||
*/
|
||||
protected function gatherLinks(array $assets, $css = true)
|
||||
{
|
||||
$buffer = '';
|
||||
|
||||
|
||||
foreach ($assets as $id => $asset) {
|
||||
$local = true;
|
||||
|
||||
$link = $asset->getAsset();
|
||||
$relative_path = $link;
|
||||
|
||||
if (static::isRemoteLink($link)) {
|
||||
$local = false;
|
||||
if (0 === strpos($link, '//')) {
|
||||
$link = 'http:' . $link;
|
||||
}
|
||||
$relative_dir = dirname($relative_path);
|
||||
} else {
|
||||
// Fix to remove relative dir if grav is in one
|
||||
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
|
||||
$base_url = '#' . preg_quote($this->base_url, '#') . '#';
|
||||
$relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
|
||||
}
|
||||
|
||||
$relative_dir = dirname($relative_path);
|
||||
$link = GRAV_ROOT . '/' . $relative_path;
|
||||
}
|
||||
|
||||
// TODO: looks like this is not being used.
|
||||
$file = $this->fetch_command instanceof Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
|
||||
|
||||
// No file found, skip it...
|
||||
if ($file === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Double check last character being
|
||||
if (!$css) {
|
||||
$file = rtrim($file, ' ;') . ';';
|
||||
}
|
||||
|
||||
// If this is CSS + the file is local + rewrite enabled
|
||||
if ($css && $this->css_rewrite) {
|
||||
$file = $this->cssRewrite($file, $relative_dir, $local);
|
||||
}
|
||||
|
||||
$file = rtrim($file) . PHP_EOL;
|
||||
$buffer .= $file;
|
||||
}
|
||||
|
||||
// Pull out @imports and move to top
|
||||
if ($css) {
|
||||
$buffer = $this->moveImports($buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves @import statements to the top of the file per the CSS specification
|
||||
*
|
||||
* @param string $file the file containing the combined CSS files
|
||||
* @return string the modified file with any @imports at the top of the file
|
||||
*/
|
||||
protected function moveImports($file)
|
||||
{
|
||||
$regex = '{@import.*?["\']([^"\']+)["\'].*?;}';
|
||||
|
||||
$imports = [];
|
||||
|
||||
$file = (string)preg_replace_callback($regex, function ($matches) use (&$imports) {
|
||||
$imports[] = $matches[0];
|
||||
|
||||
return '';
|
||||
}, $file);
|
||||
|
||||
return implode("\n", $imports) . "\n\n" . $file;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Build an HTML attribute string from an array.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderAttributes()
|
||||
{
|
||||
$html = '';
|
||||
$no_key = ['loading'];
|
||||
|
||||
foreach ($this->attributes as $key => $value) {
|
||||
if (is_numeric($key)) {
|
||||
$key = $value;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$value = implode(' ', $value);
|
||||
}
|
||||
|
||||
if (in_array($key, $no_key, true)) {
|
||||
$element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
|
||||
} else {
|
||||
$element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
|
||||
}
|
||||
|
||||
$html .= ' ' . $element;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Querystring
|
||||
*
|
||||
* @param string|null $asset
|
||||
* @return string
|
||||
*/
|
||||
protected function renderQueryString($asset = null)
|
||||
{
|
||||
$querystring = '';
|
||||
|
||||
$asset = $asset ?? $this->asset;
|
||||
|
||||
if (!empty($this->query)) {
|
||||
if (Utils::contains($asset, '?')) {
|
||||
$querystring .= '&' . $this->query;
|
||||
} else {
|
||||
$querystring .= '?' . $this->query;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->timestamp) {
|
||||
if (Utils::contains($asset, '?') || $querystring) {
|
||||
$querystring .= '&' . $this->timestamp;
|
||||
} else {
|
||||
$querystring .= '?' . $this->timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return $querystring;
|
||||
}
|
||||
}
|
||||
137
system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
Normal file
137
system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use Grav\Common\Assets;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* Trait LegacyAssetsTrait
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*/
|
||||
trait LegacyAssetsTrait
|
||||
{
|
||||
/**
|
||||
* @param array $args
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function unifyLegacyArguments($args, $type = Assets::CSS_TYPE)
|
||||
{
|
||||
// First argument is always the asset
|
||||
array_shift($args);
|
||||
|
||||
if (count($args) === 0) {
|
||||
return [];
|
||||
}
|
||||
// New options array format
|
||||
if (count($args) === 1 && is_array($args[0])) {
|
||||
return $args[0];
|
||||
}
|
||||
// Handle obscure case where options array is mixed with a priority
|
||||
if (count($args) === 2 && is_array($args[0]) && is_int($args[1])) {
|
||||
$arguments = $args[0];
|
||||
$arguments['priority'] = $args[1];
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case (Assets::JS_TYPE):
|
||||
$defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
|
||||
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
|
||||
break;
|
||||
|
||||
case (Assets::INLINE_JS_TYPE):
|
||||
$defaults = ['priority' => null, 'group' => null, 'attributes' => null];
|
||||
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
|
||||
|
||||
// special case to handle old attributes being passed in
|
||||
if (isset($arguments['attributes'])) {
|
||||
$old_attributes = $arguments['attributes'];
|
||||
if (is_array($old_attributes)) {
|
||||
$arguments = array_merge($arguments, $old_attributes);
|
||||
} else {
|
||||
$arguments['type'] = $old_attributes;
|
||||
}
|
||||
}
|
||||
unset($arguments['attributes']);
|
||||
|
||||
break;
|
||||
|
||||
case (Assets::INLINE_CSS_TYPE):
|
||||
$defaults = ['priority' => null, 'group' => null];
|
||||
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
|
||||
break;
|
||||
|
||||
default:
|
||||
case (Assets::CSS_TYPE):
|
||||
$defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
|
||||
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $args
|
||||
* @param array $defaults
|
||||
* @return array
|
||||
*/
|
||||
protected function createArgumentsFromLegacy(array $args, array $defaults)
|
||||
{
|
||||
// Remove arguments with old default values.
|
||||
$arguments = [];
|
||||
foreach ($args as $arg) {
|
||||
$default = current($defaults);
|
||||
if ($arg !== $default) {
|
||||
$arguments[key($defaults)] = $arg;
|
||||
}
|
||||
next($defaults);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for async loading of JavaScript
|
||||
*
|
||||
* @param string|array $asset
|
||||
* @param int $priority
|
||||
* @param bool $pipeline
|
||||
* @param string $group name of the group
|
||||
* @return Assets
|
||||
* @deprecated Please use dynamic method with ['loading' => 'async'].
|
||||
*/
|
||||
public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head')
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED);
|
||||
|
||||
return $this->addJs($asset, $priority, $pipeline, 'async', $group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for deferred loading of JavaScript
|
||||
*
|
||||
* @param string|array $asset
|
||||
* @param int $priority
|
||||
* @param bool $pipeline
|
||||
* @param string $group name of the group
|
||||
* @return Assets
|
||||
* @deprecated Please use dynamic method with ['loading' => 'defer'].
|
||||
*/
|
||||
public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head')
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED);
|
||||
|
||||
return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
|
||||
}
|
||||
}
|
||||
341
system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
Normal file
341
system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Grav;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Trait TestingAssetsTrait
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*/
|
||||
trait TestingAssetsTrait
|
||||
{
|
||||
/**
|
||||
* Determines if an asset exists as a collection, CSS or JS reference
|
||||
*
|
||||
* @param string $asset
|
||||
* @return bool
|
||||
*/
|
||||
public function exists($asset)
|
||||
{
|
||||
return isset($this->collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the array of all the registered collections
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCollections()
|
||||
{
|
||||
return $this->collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of collections explicitly
|
||||
*
|
||||
* @param array $collections
|
||||
* @return $this
|
||||
*/
|
||||
public function setCollection($collections)
|
||||
{
|
||||
$this->collections = $collections;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the array of all the registered CSS assets
|
||||
* If a $key is provided, it will try to return only that asset
|
||||
* else it will return null
|
||||
*
|
||||
* @param string|null $key the asset key
|
||||
* @return array
|
||||
*/
|
||||
public function getCss($key = null)
|
||||
{
|
||||
if (null !== $key) {
|
||||
$asset_key = md5($key);
|
||||
|
||||
return $this->assets_css[$asset_key] ?? null;
|
||||
}
|
||||
|
||||
return $this->assets_css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the array of all the registered JS assets
|
||||
* If a $key is provided, it will try to return only that asset
|
||||
* else it will return null
|
||||
*
|
||||
* @param string|null $key the asset key
|
||||
* @return array
|
||||
*/
|
||||
public function getJs($key = null)
|
||||
{
|
||||
if (null !== $key) {
|
||||
$asset_key = md5($key);
|
||||
|
||||
return $this->assets_js[$asset_key] ?? null;
|
||||
}
|
||||
|
||||
return $this->assets_js;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the whole array of CSS assets
|
||||
*
|
||||
* @param array $css
|
||||
* @return $this
|
||||
*/
|
||||
public function setCss($css)
|
||||
{
|
||||
$this->assets_css = $css;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the whole array of JS assets
|
||||
*
|
||||
* @param array $js
|
||||
* @return $this
|
||||
*/
|
||||
public function setJs($js)
|
||||
{
|
||||
$this->assets_js = $js;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the CSS array if set
|
||||
*
|
||||
* @param string $key The asset key
|
||||
* @return $this
|
||||
*/
|
||||
public function removeCss($key)
|
||||
{
|
||||
$asset_key = md5($key);
|
||||
if (isset($this->assets_css[$asset_key])) {
|
||||
unset($this->assets_css[$asset_key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the JS array if set
|
||||
*
|
||||
* @param string $key The asset key
|
||||
* @return $this
|
||||
*/
|
||||
public function removeJs($key)
|
||||
{
|
||||
$asset_key = md5($key);
|
||||
if (isset($this->assets_js[$asset_key])) {
|
||||
unset($this->assets_js[$asset_key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of CSS Pipeline
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setCssPipeline($value)
|
||||
{
|
||||
$this->css_pipeline = (bool)$value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of JS Pipeline
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setJsPipeline($value)
|
||||
{
|
||||
$this->js_pipeline = (bool)$value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->resetCss();
|
||||
$this->resetJs();
|
||||
$this->setCssPipeline(false);
|
||||
$this->setJsPipeline(false);
|
||||
$this->order = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset JavaScript assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function resetJs()
|
||||
{
|
||||
$this->assets_js = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset CSS assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function resetCss()
|
||||
{
|
||||
$this->assets_css = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly set's a timestamp for assets
|
||||
*
|
||||
* @param string|int $value
|
||||
*/
|
||||
public function setTimestamp($value)
|
||||
{
|
||||
$this->timestamp = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp for assets
|
||||
*
|
||||
* @param bool $include_join
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTimestamp($include_join = true)
|
||||
{
|
||||
if ($this->timestamp) {
|
||||
return $include_join ? '?' . $this->timestamp : $this->timestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all assets matching $pattern within $directory.
|
||||
*
|
||||
* @param string $directory Relative to the Grav root path, or a stream identifier
|
||||
* @param string $pattern (regex)
|
||||
* @return $this
|
||||
*/
|
||||
public function addDir($directory, $pattern = self::DEFAULT_REGEX)
|
||||
{
|
||||
$root_dir = GRAV_ROOT;
|
||||
|
||||
// Check if $directory is a stream.
|
||||
if (strpos($directory, '://')) {
|
||||
$directory = Grav::instance()['locator']->findResource($directory, null);
|
||||
}
|
||||
|
||||
// Get files
|
||||
$files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
|
||||
|
||||
// No luck? Nothing to do
|
||||
if (!$files) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Add CSS files
|
||||
if ($pattern === self::CSS_REGEX) {
|
||||
foreach ($files as $file) {
|
||||
$this->addCss($file);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Add JavaScript files
|
||||
if ($pattern === self::JS_REGEX) {
|
||||
foreach ($files as $file) {
|
||||
$this->addJs($file);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Unknown pattern.
|
||||
foreach ($files as $asset) {
|
||||
$this->add($asset);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all JavaScript assets within $directory
|
||||
*
|
||||
* @param string $directory Relative to the Grav root path, or a stream identifier
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirJs($directory)
|
||||
{
|
||||
return $this->addDir($directory, self::JS_REGEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all CSS assets within $directory
|
||||
*
|
||||
* @param string $directory Relative to the Grav root path, or a stream identifier
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirCss($directory)
|
||||
{
|
||||
return $this->addDir($directory, self::CSS_REGEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get files matching $pattern within $directory.
|
||||
*
|
||||
* @param string $directory
|
||||
* @param string $pattern (regex)
|
||||
* @param string|null $ltrim Will be trimmed from the left of the file path
|
||||
* @return array
|
||||
*/
|
||||
protected function rglob($directory, $pattern, $ltrim = null)
|
||||
{
|
||||
$iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
|
||||
$directory,
|
||||
FilesystemIterator::SKIP_DOTS
|
||||
)), $pattern);
|
||||
$offset = strlen($ltrim);
|
||||
$files = [];
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$files[] = substr($file->getPathname(), $offset);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
323
system/src/Grav/Common/Backup/Backups.php
Normal file
323
system/src/Grav/Common/Backup/Backups.php
Normal file
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Backup
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Backup;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use FilesystemIterator;
|
||||
use GlobIterator;
|
||||
use Grav\Common\Filesystem\Archiver;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Inflector;
|
||||
use Grav\Common\Scheduler\Job;
|
||||
use Grav\Common\Scheduler\Scheduler;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
use stdClass;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Class Backups
|
||||
* @package Grav\Common\Backup
|
||||
*/
|
||||
class Backups
|
||||
{
|
||||
protected const BACKUP_FILENAME_REGEXZ = "#(.*)--(\d*).zip#";
|
||||
|
||||
protected const BACKUP_DATE_FORMAT = 'YmdHis';
|
||||
|
||||
/** @var string */
|
||||
protected static $backup_dir;
|
||||
|
||||
/** @var array|null */
|
||||
protected static $backups;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = $grav['events'];
|
||||
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
|
||||
|
||||
$grav->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
if (null === static::$backup_dir) {
|
||||
$grav = Grav::instance();
|
||||
static::$backup_dir = $grav['locator']->findResource('backup://', true, true);
|
||||
Folder::create(static::$backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function onSchedulerInitialized(Event $event)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Scheduler $scheduler */
|
||||
$scheduler = $event['scheduler'];
|
||||
|
||||
/** @var Inflector $inflector */
|
||||
$inflector = $grav['inflector'];
|
||||
|
||||
foreach (static::getBackupProfiles() as $id => $profile) {
|
||||
$at = $profile['schedule_at'];
|
||||
$name = $inflector::hyphenize($profile['name']);
|
||||
$logs = 'logs/backup-' . $name . '.out';
|
||||
/** @var Job $job */
|
||||
$job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name);
|
||||
$job->at($at);
|
||||
$job->output($logs);
|
||||
$job->backlink('/tools/backups');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $backup
|
||||
* @param string $base_url
|
||||
* @return string
|
||||
*/
|
||||
public function getBackupDownloadUrl($backup, $base_url)
|
||||
{
|
||||
$param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
|
||||
$download = urlencode(base64_encode(basename($backup)));
|
||||
$url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim(
|
||||
$base_url,
|
||||
'/'
|
||||
) . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getBackupProfiles()
|
||||
{
|
||||
return Grav::instance()['config']->get('backups.profiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getPurgeConfig()
|
||||
{
|
||||
return Grav::instance()['config']->get('backups.purge');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getBackupNames()
|
||||
{
|
||||
return array_column(static::getBackupProfiles(), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int
|
||||
*/
|
||||
public static function getTotalBackupsSize()
|
||||
{
|
||||
$backups = static::getAvailableBackups();
|
||||
$size = array_sum(array_column($backups, 'size'));
|
||||
|
||||
return $size ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $force
|
||||
* @return array
|
||||
*/
|
||||
public static function getAvailableBackups($force = false)
|
||||
{
|
||||
if ($force || null === static::$backups) {
|
||||
static::$backups = [];
|
||||
|
||||
$grav = Grav::instance();
|
||||
$backups_itr = new GlobIterator(static::$backup_dir . '/*.zip', FilesystemIterator::KEY_AS_FILENAME);
|
||||
$inflector = $grav['inflector'];
|
||||
$long_date_format = DATE_RFC2822;
|
||||
|
||||
/**
|
||||
* @var string $name
|
||||
* @var SplFileInfo $file
|
||||
*/
|
||||
foreach ($backups_itr as $name => $file) {
|
||||
if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) {
|
||||
$date = DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]);
|
||||
$timestamp = $date->getTimestamp();
|
||||
$backup = new stdClass();
|
||||
$backup->title = $inflector->titleize($matches[1]);
|
||||
$backup->time = $date;
|
||||
$backup->date = $date->format($long_date_format);
|
||||
$backup->filename = $name;
|
||||
$backup->path = $file->getPathname();
|
||||
$backup->size = $file->getSize();
|
||||
static::$backups[$timestamp] = $backup;
|
||||
}
|
||||
}
|
||||
// Reverse Key Sort to get in reverse date order
|
||||
krsort(static::$backups);
|
||||
}
|
||||
|
||||
return static::$backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup
|
||||
*
|
||||
* @param int $id
|
||||
* @param callable|null $status
|
||||
* @return string|null
|
||||
*/
|
||||
public static function backup($id = 0, callable $status = null)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
$profiles = static::getBackupProfiles();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
if (isset($profiles[$id])) {
|
||||
$backup = (object) $profiles[$id];
|
||||
} else {
|
||||
throw new RuntimeException('No backups defined...');
|
||||
}
|
||||
|
||||
$name = $grav['inflector']->underscorize($backup->name);
|
||||
$date = date(static::BACKUP_DATE_FORMAT, time());
|
||||
$filename = trim($name, '_') . '--' . $date . '.zip';
|
||||
$destination = static::$backup_dir . DS . $filename;
|
||||
$max_execution_time = ini_set('max_execution_time', '600');
|
||||
$backup_root = $backup->root;
|
||||
|
||||
if ($locator->isStream($backup_root)) {
|
||||
$backup_root = $locator->findResource($backup_root);
|
||||
} else {
|
||||
$backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
|
||||
}
|
||||
|
||||
if (!file_exists($backup_root)) {
|
||||
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
|
||||
}
|
||||
|
||||
$options = [
|
||||
'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),
|
||||
'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),
|
||||
];
|
||||
|
||||
$archiver = Archiver::create('zip');
|
||||
$archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status);
|
||||
|
||||
$status && $status([
|
||||
'type' => 'message',
|
||||
'message' => 'Done...',
|
||||
]);
|
||||
|
||||
$status && $status([
|
||||
'type' => 'progress',
|
||||
'complete' => true
|
||||
]);
|
||||
|
||||
if ($max_execution_time !== false) {
|
||||
ini_set('max_execution_time', $max_execution_time);
|
||||
}
|
||||
|
||||
// Log the backup
|
||||
$grav['log']->notice('Backup Created: ' . $destination);
|
||||
|
||||
// Fire Finished event
|
||||
$grav->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
|
||||
|
||||
// Purge anything required
|
||||
static::purge();
|
||||
|
||||
// Log
|
||||
$log = JsonFile::instance($locator->findResource("log://backup.log", true, true));
|
||||
$log->content([
|
||||
'time' => time(),
|
||||
'location' => $destination
|
||||
]);
|
||||
$log->save();
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function purge()
|
||||
{
|
||||
$purge_config = static::getPurgeConfig();
|
||||
$trigger = $purge_config['trigger'];
|
||||
$backups = static::getAvailableBackups(true);
|
||||
|
||||
switch ($trigger) {
|
||||
case 'number':
|
||||
$backups_count = count($backups);
|
||||
if ($backups_count > $purge_config['max_backups_count']) {
|
||||
$last = end($backups);
|
||||
unlink($last->path);
|
||||
static::purge();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'time':
|
||||
$last = end($backups);
|
||||
$now = new DateTime();
|
||||
$interval = $now->diff($last->time);
|
||||
if ($interval->days > $purge_config['max_backups_time']) {
|
||||
unlink($last->path);
|
||||
static::purge();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$used_space = static::getTotalBackupsSize();
|
||||
$max_space = $purge_config['max_backups_space'] * 1024 * 1024 * 1024;
|
||||
if ($used_space > $max_space) {
|
||||
$last = end($backups);
|
||||
unlink($last->path);
|
||||
static::purge();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $exclude
|
||||
* @return array
|
||||
*/
|
||||
protected static function convertExclude($exclude)
|
||||
{
|
||||
$lines = preg_split("/[\s,]+/", $exclude);
|
||||
|
||||
return array_map('trim', $lines, array_fill(0, count($lines), '/'));
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Grav.Common.Backup
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Backup;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Inflector;
|
||||
|
||||
class ZipBackup
|
||||
{
|
||||
protected static $ignorePaths = [
|
||||
'backup',
|
||||
'cache',
|
||||
'images',
|
||||
'logs',
|
||||
'tmp'
|
||||
];
|
||||
|
||||
protected static $ignoreFolders = [
|
||||
'.git',
|
||||
'.svn',
|
||||
'.hg',
|
||||
'.idea',
|
||||
'node_modules'
|
||||
];
|
||||
|
||||
/**
|
||||
* Backup
|
||||
*
|
||||
* @param string|null $destination
|
||||
* @param callable|null $messager
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public static function backup($destination = null, callable $messager = null)
|
||||
{
|
||||
if (!$destination) {
|
||||
$destination = Grav::instance()['locator']->findResource('backup://', true);
|
||||
|
||||
if (!$destination) {
|
||||
throw new \RuntimeException('The backup folder is missing.');
|
||||
}
|
||||
}
|
||||
|
||||
$name = substr(strip_tags(Grav::instance()['config']->get('site.title', basename(GRAV_ROOT))), 0, 20);
|
||||
|
||||
$inflector = new Inflector();
|
||||
|
||||
if (is_dir($destination)) {
|
||||
$date = date('YmdHis', time());
|
||||
$filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip';
|
||||
$destination = rtrim($destination, DS) . DS . $filename;
|
||||
}
|
||||
|
||||
$messager && $messager([
|
||||
'type' => 'message',
|
||||
'level' => 'info',
|
||||
'message' => 'Creating new Backup "' . $destination . '"'
|
||||
]);
|
||||
$messager && $messager([
|
||||
'type' => 'message',
|
||||
'level' => 'info',
|
||||
'message' => ''
|
||||
]);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
$zip->open($destination, \ZipArchive::CREATE);
|
||||
|
||||
$max_execution_time = ini_set('max_execution_time', 600);
|
||||
|
||||
static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
|
||||
|
||||
$messager && $messager([
|
||||
'type' => 'progress',
|
||||
'percentage' => false,
|
||||
'complete' => true
|
||||
]);
|
||||
|
||||
$messager && $messager([
|
||||
'type' => 'message',
|
||||
'level' => 'info',
|
||||
'message' => ''
|
||||
]);
|
||||
$messager && $messager([
|
||||
'type' => 'message',
|
||||
'level' => 'info',
|
||||
'message' => 'Saving and compressing archive...'
|
||||
]);
|
||||
|
||||
$zip->close();
|
||||
|
||||
if ($max_execution_time !== false) {
|
||||
ini_set('max_execution_time', $max_execution_time);
|
||||
}
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $folder
|
||||
* @param $zipFile
|
||||
* @param $exclusiveLength
|
||||
* @param $messager
|
||||
*/
|
||||
private static function folderToZip($folder, \ZipArchive $zipFile, $exclusiveLength, callable $messager = null)
|
||||
{
|
||||
$handle = opendir($folder);
|
||||
while (false !== $f = readdir($handle)) {
|
||||
if ($f !== '.' && $f !== '..') {
|
||||
$filePath = "$folder/$f";
|
||||
// Remove prefix from file path before add to zip.
|
||||
$localPath = substr($filePath, $exclusiveLength);
|
||||
|
||||
if (in_array($f, static::$ignoreFolders)) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($localPath, static::$ignorePaths)) {
|
||||
$zipFile->addEmptyDir($f);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_file($filePath)) {
|
||||
$zipFile->addFile($filePath, $localPath);
|
||||
|
||||
$messager && $messager([
|
||||
'type' => 'progress',
|
||||
'percentage' => false,
|
||||
'complete' => false
|
||||
]);
|
||||
} elseif (is_dir($filePath)) {
|
||||
// Add sub-directory.
|
||||
$zipFile->addEmptyDir($localPath);
|
||||
static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function donatj\UserAgent\parse_user_agent;
|
||||
|
||||
/**
|
||||
* Internally uses the PhpUserAgent package https://github.com/donatj/PhpUserAgent
|
||||
*/
|
||||
class Browser
|
||||
{
|
||||
/** @var string[] */
|
||||
protected $useragent = [];
|
||||
|
||||
/**
|
||||
@@ -22,7 +27,7 @@ class Browser
|
||||
{
|
||||
try {
|
||||
$this->useragent = parse_user_agent();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
|
||||
}
|
||||
}
|
||||
@@ -107,13 +112,13 @@ class Browser
|
||||
/**
|
||||
* Get the current major version identifier
|
||||
*
|
||||
* @return string the browser major version identifier
|
||||
* @return int the browser major version identifier
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
$version = explode('.', $this->getLongVersion());
|
||||
|
||||
return intval($version[0]);
|
||||
return (int)$version[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,4 +139,15 @@ class Browser
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if “Do Not Track” is set by browser
|
||||
* @see https://www.w3.org/TR/tracking-dnt/
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isTrackable(): bool
|
||||
{
|
||||
return !(isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] === '1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use DirectoryIterator;
|
||||
use \Doctrine\Common\Cache as DoctrineCache;
|
||||
use Exception;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Scheduler\Scheduler;
|
||||
use LogicException;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use function dirname;
|
||||
use function extension_loaded;
|
||||
use function function_exists;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* The GravCache object is used throughout Grav to store and retrieve cached data.
|
||||
* It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
|
||||
*
|
||||
* APCu
|
||||
* APC
|
||||
* XCache
|
||||
* RedisCache
|
||||
* MemCache
|
||||
* MemCacheD
|
||||
@@ -27,37 +37,41 @@ use RocketTheme\Toolbox\Event\Event;
|
||||
*/
|
||||
class Cache extends Getters
|
||||
{
|
||||
/**
|
||||
* @var string Cache key.
|
||||
*/
|
||||
/** @var string Cache key. */
|
||||
protected $key;
|
||||
|
||||
/** @var int */
|
||||
protected $lifetime;
|
||||
|
||||
/** @var int */
|
||||
protected $now;
|
||||
|
||||
/** @var Config $config */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var DoctrineCache\CacheProvider
|
||||
*/
|
||||
/** @var DoctrineCache\CacheProvider */
|
||||
protected $driver;
|
||||
|
||||
/** @var CacheInterface */
|
||||
protected $simpleCache;
|
||||
|
||||
/** @var string */
|
||||
protected $driver_name;
|
||||
|
||||
/** @var string */
|
||||
protected $driver_setting;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
/** @var bool */
|
||||
protected $enabled;
|
||||
|
||||
/** @var string */
|
||||
protected $cache_dir;
|
||||
|
||||
protected static $standard_remove = [
|
||||
'cache://twig/',
|
||||
'cache://doctrine/',
|
||||
'cache://compiled/',
|
||||
'cache://clockwork/',
|
||||
'cache://validated-',
|
||||
'cache://images',
|
||||
'asset://',
|
||||
@@ -67,6 +81,7 @@ class Cache extends Getters
|
||||
'cache://twig/',
|
||||
'cache://doctrine/',
|
||||
'cache://compiled/',
|
||||
'cache://clockwork/',
|
||||
'cache://validated-',
|
||||
'asset://',
|
||||
];
|
||||
@@ -108,46 +123,85 @@ class Cache extends Getters
|
||||
* Initialization that sets a base key and the driver based on configuration settings
|
||||
*
|
||||
* @param Grav $grav
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(Grav $grav)
|
||||
{
|
||||
/** @var Config $config */
|
||||
$this->config = $grav['config'];
|
||||
$this->now = time();
|
||||
|
||||
$this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
|
||||
if (null === $this->enabled) {
|
||||
$this->enabled = (bool)$this->config->get('system.cache.enabled');
|
||||
}
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
|
||||
$prefix = $this->config->get('system.cache.prefix');
|
||||
|
||||
if (is_null($this->enabled)) {
|
||||
$this->enabled = (bool)$this->config->get('system.cache.enabled');
|
||||
}
|
||||
$uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
|
||||
|
||||
// Cache key allows us to invalidate all cache on configuration changes.
|
||||
$this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
|
||||
2, 8);
|
||||
|
||||
$this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
|
||||
$this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
|
||||
$this->driver_setting = $this->config->get('system.cache.driver');
|
||||
|
||||
$this->driver = $this->getCacheDriver();
|
||||
|
||||
// Set the cache namespace to our unique key
|
||||
$this->driver->setNamespace($this->key);
|
||||
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = Grav::instance()['events'];
|
||||
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getSimpleCache()
|
||||
{
|
||||
if (null === $this->simpleCache) {
|
||||
$cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime());
|
||||
|
||||
// Disable cache key validation.
|
||||
$cache->setValidation(false);
|
||||
|
||||
$this->simpleCache = $cache;
|
||||
}
|
||||
|
||||
return $this->simpleCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the old out of date file-based caches
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function purgeOldCache()
|
||||
{
|
||||
$cache_dir = dirname($this->cache_dir);
|
||||
$current = basename($this->cache_dir);
|
||||
$count = 0;
|
||||
|
||||
foreach (new DirectoryIterator($cache_dir) as $file) {
|
||||
$dir = $file->getBasename();
|
||||
if ($dir === $current || $file->isDot() || $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Folder::delete($file->getPathname());
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public accessor to set the enabled state of the cache
|
||||
*
|
||||
* @param $enabled
|
||||
* @param bool|int $enabled
|
||||
* @return void
|
||||
*/
|
||||
public function setEnabled($enabled)
|
||||
{
|
||||
$this->enabled = (bool) $enabled;
|
||||
$this->enabled = (bool)$enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,19 +238,15 @@ class Cache extends Getters
|
||||
|
||||
// CLI compatibility requires a non-volatile cache driver
|
||||
if ($this->config->get('system.cache.cli_compatibility') && (
|
||||
$setting == 'auto' || $this->isVolatileDriver($setting))) {
|
||||
$setting === 'auto' || $this->isVolatileDriver($setting))) {
|
||||
$setting = $driver_name;
|
||||
}
|
||||
|
||||
if (!$setting || $setting == 'auto') {
|
||||
if (!$setting || $setting === 'auto') {
|
||||
if (extension_loaded('apcu')) {
|
||||
$driver_name = 'apcu';
|
||||
} elseif (extension_loaded('apc')) {
|
||||
$driver_name = 'apc';
|
||||
} elseif (extension_loaded('wincache')) {
|
||||
$driver_name = 'wincache';
|
||||
} elseif (extension_loaded('xcache')) {
|
||||
$driver_name = 'xcache';
|
||||
}
|
||||
} else {
|
||||
$driver_name = $setting;
|
||||
@@ -206,9 +256,6 @@ class Cache extends Getters
|
||||
|
||||
switch ($driver_name) {
|
||||
case 'apc':
|
||||
$driver = new DoctrineCache\ApcCache();
|
||||
break;
|
||||
|
||||
case 'apcu':
|
||||
$driver = new DoctrineCache\ApcuCache();
|
||||
break;
|
||||
@@ -217,45 +264,65 @@ class Cache extends Getters
|
||||
$driver = new DoctrineCache\WinCacheCache();
|
||||
break;
|
||||
|
||||
case 'xcache':
|
||||
$driver = new DoctrineCache\XcacheCache();
|
||||
break;
|
||||
|
||||
case 'memcache':
|
||||
$memcache = new \Memcache();
|
||||
$memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
|
||||
$this->config->get('system.cache.memcache.port', 11211));
|
||||
$driver = new DoctrineCache\MemcacheCache();
|
||||
$driver->setMemcache($memcache);
|
||||
if (extension_loaded('memcache')) {
|
||||
$memcache = new \Memcache();
|
||||
$memcache->connect(
|
||||
$this->config->get('system.cache.memcache.server', 'localhost'),
|
||||
$this->config->get('system.cache.memcache.port', 11211)
|
||||
);
|
||||
$driver = new DoctrineCache\MemcacheCache();
|
||||
$driver->setMemcache($memcache);
|
||||
} else {
|
||||
throw new LogicException('Memcache PHP extension has not been installed');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
|
||||
$this->config->get('system.cache.memcached.port', 11211));
|
||||
$driver = new DoctrineCache\MemcachedCache();
|
||||
$driver->setMemcached($memcached);
|
||||
if (extension_loaded('memcached')) {
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(
|
||||
$this->config->get('system.cache.memcached.server', 'localhost'),
|
||||
$this->config->get('system.cache.memcached.port', 11211)
|
||||
);
|
||||
$driver = new DoctrineCache\MemcachedCache();
|
||||
$driver->setMemcached($memcached);
|
||||
} else {
|
||||
throw new LogicException('Memcached PHP extension has not been installed');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'redis':
|
||||
$redis = new \Redis();
|
||||
$socket = $this->config->get('system.cache.redis.socket', false);
|
||||
$password = $this->config->get('system.cache.redis.password', false);
|
||||
if (extension_loaded('redis')) {
|
||||
$redis = new \Redis();
|
||||
$socket = $this->config->get('system.cache.redis.socket', false);
|
||||
$password = $this->config->get('system.cache.redis.password', false);
|
||||
$databaseId = $this->config->get('system.cache.redis.database', 0);
|
||||
|
||||
if ($socket) {
|
||||
$redis->connect($socket);
|
||||
if ($socket) {
|
||||
$redis->connect($socket);
|
||||
} else {
|
||||
$redis->connect(
|
||||
$this->config->get('system.cache.redis.server', 'localhost'),
|
||||
$this->config->get('system.cache.redis.port', 6379)
|
||||
);
|
||||
}
|
||||
|
||||
// Authenticate with password if set
|
||||
if ($password && !$redis->auth($password)) {
|
||||
throw new \RedisException('Redis authentication failed');
|
||||
}
|
||||
|
||||
// Select alternate ( !=0 ) database ID if set
|
||||
if ($databaseId && !$redis->select($databaseId)) {
|
||||
throw new \RedisException('Could not select alternate Redis database ID');
|
||||
}
|
||||
|
||||
$driver = new DoctrineCache\RedisCache();
|
||||
$driver->setRedis($redis);
|
||||
} else {
|
||||
$redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
|
||||
$this->config->get('system.cache.redis.port', 6379));
|
||||
throw new LogicException('Redis PHP extension has not been installed');
|
||||
}
|
||||
|
||||
// Authenticate with password if set
|
||||
if ($password && !$redis->auth($password)) {
|
||||
throw new \RedisException('Redis authentication failed');
|
||||
}
|
||||
|
||||
$driver = new DoctrineCache\RedisCache();
|
||||
$driver->setRedis($redis);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -270,24 +337,23 @@ class Cache extends Getters
|
||||
* Gets a cached entry if it exists based on an id. If it does not exist, it returns false
|
||||
*
|
||||
* @param string $id the id of the cached entry
|
||||
*
|
||||
* @return object|bool returns the cached entry, can be any type, or false if doesn't exist
|
||||
* @return mixed|bool returns the cached entry, can be any type, or false if doesn't exist
|
||||
*/
|
||||
public function fetch($id)
|
||||
{
|
||||
if ($this->enabled) {
|
||||
return $this->driver->fetch($id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new cached entry.
|
||||
*
|
||||
* @param string $id the id of the cached entry
|
||||
* @param array|object $data the data for the cached entry to store
|
||||
* @param int $lifetime the lifetime to store the entry in seconds
|
||||
* @param array|object|int $data the data for the cached entry to store
|
||||
* @param int|null $lifetime the lifetime to store the entry in seconds
|
||||
*/
|
||||
public function save($id, $data, $lifetime = null)
|
||||
{
|
||||
@@ -310,6 +376,21 @@ class Cache extends Getters
|
||||
if ($this->enabled) {
|
||||
return $this->driver->delete($id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all cache
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteAll()
|
||||
{
|
||||
if ($this->enabled) {
|
||||
return $this->driver->deleteAll();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -324,11 +405,14 @@ class Cache extends Getters
|
||||
if ($this->enabled) {
|
||||
return $this->driver->contains(($id));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method to get the cache key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
@@ -337,6 +421,9 @@ class Cache extends Getters
|
||||
|
||||
/**
|
||||
* Setter method to set key (Advanced)
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
public function setKey($key)
|
||||
{
|
||||
@@ -348,7 +435,6 @@ class Cache extends Getters
|
||||
* Helper method to clear all Grav caches
|
||||
*
|
||||
* @param string $remove standard|all|assets-only|images-only|cache-only
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function clearCache($remove = 'standard')
|
||||
@@ -373,24 +459,33 @@ class Cache extends Getters
|
||||
case 'tmp-only':
|
||||
$remove_paths = self::$tmp_remove;
|
||||
break;
|
||||
case 'invalidate':
|
||||
$remove_paths = [];
|
||||
break;
|
||||
default:
|
||||
if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
|
||||
$remove_paths = self::$standard_remove;
|
||||
} else {
|
||||
$remove_paths = self::$standard_remove_no_images;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete entries in the doctrine cache if required
|
||||
if (in_array($remove, ['all', 'standard'])) {
|
||||
$cache = Grav::instance()['cache'];
|
||||
$cache->driver->deleteAll();
|
||||
}
|
||||
|
||||
// Clearing cache event to add paths to clear
|
||||
Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
|
||||
|
||||
foreach ($remove_paths as $stream) {
|
||||
|
||||
// Convert stream to a real path
|
||||
try {
|
||||
$path = $locator->findResource($stream, true, true);
|
||||
if($path === false) continue;
|
||||
if ($path === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$anything = false;
|
||||
$files = glob($path . '/*');
|
||||
@@ -404,7 +499,7 @@ class Cache extends Getters
|
||||
$anything = true;
|
||||
}
|
||||
} elseif (is_dir($file)) {
|
||||
if (Folder::delete($file)) {
|
||||
if (Folder::delete($file, false)) {
|
||||
$anything = true;
|
||||
}
|
||||
}
|
||||
@@ -414,7 +509,7 @@ class Cache extends Getters
|
||||
if ($anything) {
|
||||
$output[] = '<red>Cleared: </red>' . $path . '/*';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
// stream not found or another error while deleting files.
|
||||
$output[] = '<red>ERROR: </red>' . $e->getMessage();
|
||||
}
|
||||
@@ -422,7 +517,7 @@ class Cache extends Getters
|
||||
|
||||
$output[] = '';
|
||||
|
||||
if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
|
||||
if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) {
|
||||
touch($user_config);
|
||||
|
||||
$output[] = '<red>Touched: </red>' . $user_config;
|
||||
@@ -437,14 +532,36 @@ class Cache extends Getters
|
||||
@opcache_reset();
|
||||
}
|
||||
|
||||
Grav::instance()->fireEvent('onAfterCacheClear', new Event(['remove' => $remove, 'output' => &$output]));
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidateCache()
|
||||
{
|
||||
$user_config = USER_DIR . 'config/system.yaml';
|
||||
|
||||
if (file_exists($user_config)) {
|
||||
touch($user_config);
|
||||
}
|
||||
|
||||
// Clear stat cache
|
||||
@clearstatcache();
|
||||
|
||||
// Clear opcache
|
||||
if (function_exists('opcache_reset')) {
|
||||
@opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache lifetime programmatically
|
||||
*
|
||||
* @param int $future timestamp
|
||||
* @return void
|
||||
*/
|
||||
public function setLifetime($future)
|
||||
{
|
||||
@@ -452,7 +569,7 @@ class Cache extends Getters
|
||||
return;
|
||||
}
|
||||
|
||||
$interval = $future - $this->now;
|
||||
$interval = (int)($future - $this->now);
|
||||
if ($interval > 0 && $interval < $this->getLifetime()) {
|
||||
$this->lifetime = $interval;
|
||||
}
|
||||
@@ -462,12 +579,12 @@ class Cache extends Getters
|
||||
/**
|
||||
* Retrieve the cache lifetime (in seconds)
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function getLifetime()
|
||||
{
|
||||
if ($this->lifetime === null) {
|
||||
$this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default
|
||||
$this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
|
||||
}
|
||||
|
||||
return $this->lifetime;
|
||||
@@ -476,7 +593,7 @@ class Cache extends Getters
|
||||
/**
|
||||
* Returns the current driver name
|
||||
*
|
||||
* @return mixed
|
||||
* @return string
|
||||
*/
|
||||
public function getDriverName()
|
||||
{
|
||||
@@ -486,7 +603,7 @@ class Cache extends Getters
|
||||
/**
|
||||
* Returns the current driver setting
|
||||
*
|
||||
* @return mixed
|
||||
* @return string
|
||||
*/
|
||||
public function getDriverSetting()
|
||||
{
|
||||
@@ -496,15 +613,82 @@ class Cache extends Getters
|
||||
/**
|
||||
* is this driver a volatile driver in that it resides in PHP process memory
|
||||
*
|
||||
* @param $setting
|
||||
* @param string $setting
|
||||
* @return bool
|
||||
*/
|
||||
public function isVolatileDriver($setting)
|
||||
{
|
||||
if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to call as a scheduled Job to purge old Doctrine files
|
||||
*
|
||||
* @param bool $echo
|
||||
*
|
||||
* @return string|void
|
||||
*/
|
||||
public static function purgeJob($echo = false)
|
||||
{
|
||||
/** @var Cache $cache */
|
||||
$cache = Grav::instance()['cache'];
|
||||
$deleted_folders = $cache->purgeOldCache();
|
||||
$msg = 'Purged ' . $deleted_folders . ' old cache folders...';
|
||||
|
||||
if ($echo) {
|
||||
echo $msg;
|
||||
} else {
|
||||
return false;
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to call as a scheduled Job to clear Grav cache
|
||||
*
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public static function clearJob($type)
|
||||
{
|
||||
$result = static::clearCache($type);
|
||||
static::invalidateCache();
|
||||
|
||||
echo strip_tags(implode("\n", $result));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function onSchedulerInitialized(Event $event)
|
||||
{
|
||||
/** @var Scheduler $scheduler */
|
||||
$scheduler = $event['scheduler'];
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
// File Cache Purge
|
||||
$at = $config->get('system.cache.purge_at');
|
||||
$name = 'cache-purge';
|
||||
$logs = 'logs/' . $name . '.out';
|
||||
|
||||
$job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [true], $name);
|
||||
$job->at($at);
|
||||
$job->output($logs);
|
||||
$job->backlink('/config/system#caching');
|
||||
|
||||
// Cache Clear
|
||||
$at = $config->get('system.cache.clear_at');
|
||||
$clear_type = $config->get('system.cache.clear_job_type');
|
||||
$name = 'cache-clear';
|
||||
$logs = 'logs/' . $name . '.out';
|
||||
|
||||
$job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name);
|
||||
$job->at($at);
|
||||
$job->output($logs);
|
||||
$job->backlink('/config/system#caching');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use function function_exists;
|
||||
|
||||
/**
|
||||
* Class Composer
|
||||
* @package Grav\Common
|
||||
*/
|
||||
class Composer
|
||||
{
|
||||
/** @const Default composer location */
|
||||
const DEFAULT_PATH = "bin/composer.phar";
|
||||
const DEFAULT_PATH = 'bin/composer.phar';
|
||||
|
||||
/**
|
||||
* Returns the location of composer.
|
||||
@@ -20,12 +27,12 @@ class Composer
|
||||
*/
|
||||
public static function getComposerLocation()
|
||||
{
|
||||
if (!function_exists('shell_exec') || strtolower(substr(PHP_OS, 0, 3)) === 'win') {
|
||||
if (!function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
|
||||
return self::DEFAULT_PATH;
|
||||
}
|
||||
|
||||
// check for global composer install
|
||||
$path = trim(shell_exec("command -v composer"));
|
||||
$path = trim((string)shell_exec('command -v composer'));
|
||||
|
||||
// fall back to grav bundled composer
|
||||
if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
|
||||
@@ -46,7 +53,7 @@ class Composer
|
||||
$composer = static::getComposerLocation();
|
||||
|
||||
if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
|
||||
$file = fopen($composer, 'r');
|
||||
$file = fopen($composer, 'rb');
|
||||
$firstLine = fgets($file);
|
||||
fclose($file);
|
||||
|
||||
|
||||
@@ -1,79 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Exception;
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
use RuntimeException;
|
||||
use function get_class;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class CompiledBase
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
abstract class CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var int Version number for the compiled file.
|
||||
*/
|
||||
/** @var int Version number for the compiled file. */
|
||||
public $version = 1;
|
||||
|
||||
/**
|
||||
* @var string Filename (base name) of the compiled configuration.
|
||||
*/
|
||||
/** @var string Filename (base name) of the compiled configuration. */
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string|bool Configuration checksum.
|
||||
*/
|
||||
/** @var string|bool Configuration checksum. */
|
||||
public $checksum;
|
||||
|
||||
/**
|
||||
* @var string Timestamp of compiled configuration
|
||||
*/
|
||||
public $timestamp;
|
||||
/** @var int Timestamp of compiled configuration */
|
||||
public $timestamp = 0;
|
||||
|
||||
/**
|
||||
* @var string Cache folder to be used.
|
||||
*/
|
||||
/** @var string Cache folder to be used. */
|
||||
protected $cacheFolder;
|
||||
|
||||
/**
|
||||
* @var array List of files to load.
|
||||
*/
|
||||
/** @var array List of files to load. */
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var mixed Configuration object.
|
||||
*/
|
||||
/** @var mixed Configuration object. */
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* @param string $cacheFolder Cache folder to be used.
|
||||
* @param array $files List of files as returned from ConfigFileFinder class.
|
||||
* @param string $path Base path for the file list.
|
||||
* @throws \BadMethodCallException
|
||||
* @throws BadMethodCallException
|
||||
*/
|
||||
public function __construct($cacheFolder, array $files, $path)
|
||||
{
|
||||
if (!$cacheFolder) {
|
||||
throw new \BadMethodCallException('Cache folder not defined.');
|
||||
throw new BadMethodCallException('Cache folder not defined.');
|
||||
}
|
||||
|
||||
$this->path = $path ? rtrim($path, '\\/') . '/' : '';
|
||||
$this->cacheFolder = $cacheFolder;
|
||||
$this->files = $files;
|
||||
$this->timestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename for the compiled PHP file.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $name
|
||||
* @return $this
|
||||
*/
|
||||
public function name($name = null)
|
||||
@@ -87,8 +80,12 @@ abstract class CompiledBase
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modified() {}
|
||||
public function modified()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of compiled configuration
|
||||
@@ -128,13 +125,16 @@ abstract class CompiledBase
|
||||
*/
|
||||
public function checksum()
|
||||
{
|
||||
if (!isset($this->checksum)) {
|
||||
if (null === $this->checksum) {
|
||||
$this->checksum = md5(json_encode($this->files) . $this->version);
|
||||
}
|
||||
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function createFilename()
|
||||
{
|
||||
return "{$this->cacheFolder}/{$this->name()->name}.php";
|
||||
@@ -144,11 +144,14 @@ abstract class CompiledBase
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function createObject(array $data = []);
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function finalizeObject();
|
||||
|
||||
@@ -156,7 +159,8 @@ abstract class CompiledBase
|
||||
* Load single configuration file and append it to the correct position.
|
||||
*
|
||||
* @param string $name Name of the position.
|
||||
* @param string $filename File to be loaded.
|
||||
* @param string|string[] $filename File(s) to be loaded.
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function loadFile($name, $filename);
|
||||
|
||||
@@ -196,12 +200,9 @@ abstract class CompiledBase
|
||||
}
|
||||
|
||||
$cache = include $filename;
|
||||
if (
|
||||
!is_array($cache)
|
||||
|| !isset($cache['checksum'])
|
||||
|| !isset($cache['data'])
|
||||
|| !isset($cache['@class'])
|
||||
|| $cache['@class'] != get_class($this)
|
||||
if (!is_array($cache)
|
||||
|| !isset($cache['checksum'], $cache['data'], $cache['@class'])
|
||||
|| $cache['@class'] !== get_class($this)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -212,7 +213,7 @@ abstract class CompiledBase
|
||||
}
|
||||
|
||||
$this->createObject($cache['data']);
|
||||
$this->timestamp = isset($cache['timestamp']) ? $cache['timestamp'] : 0;
|
||||
$this->timestamp = $cache['timestamp'] ?? 0;
|
||||
|
||||
$this->finalizeObject();
|
||||
|
||||
@@ -223,7 +224,8 @@ abstract class CompiledBase
|
||||
* Save compiled file.
|
||||
*
|
||||
* @param string $filename
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
protected function saveCompiledFile($filename)
|
||||
@@ -233,7 +235,7 @@ abstract class CompiledBase
|
||||
// Attempt to lock the file for writing.
|
||||
try {
|
||||
$file->lock(false);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
// Another process has locked the file; we will check this in a bit.
|
||||
}
|
||||
|
||||
@@ -257,6 +259,9 @@ abstract class CompiledBase
|
||||
$this->modified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getState()
|
||||
{
|
||||
return $this->object->toArray();
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Data\BlueprintSchema;
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Class CompiledBlueprints
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class CompiledBlueprints extends CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var int Version number for the compiled file.
|
||||
* CompiledBlueprints constructor.
|
||||
* @param string $cacheFolder
|
||||
* @param array $files
|
||||
* @param string $path
|
||||
*/
|
||||
public $version = 2;
|
||||
public function __construct($cacheFolder, array $files, $path)
|
||||
{
|
||||
parent::__construct($cacheFolder, $files, $path);
|
||||
|
||||
/**
|
||||
* @var BlueprintSchema Blueprints object.
|
||||
*/
|
||||
protected $object;
|
||||
$this->version = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns checksum from the configuration files.
|
||||
@@ -42,7 +51,7 @@ class CompiledBlueprints extends CompiledBase
|
||||
/**
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $data
|
||||
*/
|
||||
protected function createObject(array $data = [])
|
||||
{
|
||||
@@ -61,6 +70,8 @@ class CompiledBlueprints extends CompiledBase
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function finalizeObject()
|
||||
{
|
||||
@@ -71,6 +82,7 @@ class CompiledBlueprints extends CompiledBase
|
||||
*
|
||||
* @param string $name Name of the position.
|
||||
* @param array $files Files to be loaded.
|
||||
* @return void
|
||||
*/
|
||||
protected function loadFile($name, $files)
|
||||
{
|
||||
@@ -109,6 +121,9 @@ class CompiledBlueprints extends CompiledBase
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getState()
|
||||
{
|
||||
return $this->object->getState();
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use function is_callable;
|
||||
|
||||
/**
|
||||
* Class CompiledConfig
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class CompiledConfig extends CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var int Version number for the compiled file.
|
||||
*/
|
||||
public $version = 1;
|
||||
|
||||
/**
|
||||
* @var Config Configuration object.
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* @var callable Blueprints loader.
|
||||
*/
|
||||
/** @var callable Blueprints loader. */
|
||||
protected $callable;
|
||||
|
||||
/** @var bool */
|
||||
protected $withDefaults = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
* CompiledConfig constructor.
|
||||
* @param string $cacheFolder
|
||||
* @param array $files
|
||||
* @param string $path
|
||||
*/
|
||||
protected $withDefaults;
|
||||
public function __construct($cacheFolder, array $files, $path)
|
||||
{
|
||||
parent::__construct($cacheFolder, $files, $path);
|
||||
|
||||
$this->version = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set blueprints for the configuration.
|
||||
@@ -60,6 +65,7 @@ class CompiledConfig extends CompiledBase
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function createObject(array $data = [])
|
||||
{
|
||||
@@ -73,6 +79,8 @@ class CompiledConfig extends CompiledBase
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function finalizeObject()
|
||||
{
|
||||
@@ -82,6 +90,8 @@ class CompiledConfig extends CompiledBase
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modified()
|
||||
{
|
||||
@@ -93,6 +103,7 @@ class CompiledConfig extends CompiledBase
|
||||
*
|
||||
* @param string $name Name of the position.
|
||||
* @param string $filename File to be loaded.
|
||||
* @return void
|
||||
*/
|
||||
protected function loadFile($name, $filename)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,22 +11,30 @@ namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
|
||||
/**
|
||||
* Class CompiledLanguages
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class CompiledLanguages extends CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var int Version number for the compiled file.
|
||||
* CompiledLanguages constructor.
|
||||
* @param string $cacheFolder
|
||||
* @param array $files
|
||||
* @param string $path
|
||||
*/
|
||||
public $version = 1;
|
||||
public function __construct($cacheFolder, array $files, $path)
|
||||
{
|
||||
parent::__construct($cacheFolder, $files, $path);
|
||||
|
||||
/**
|
||||
* @var Languages Configuration object.
|
||||
*/
|
||||
protected $object;
|
||||
$this->version = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function createObject(array $data = [])
|
||||
{
|
||||
@@ -34,6 +43,8 @@ class CompiledLanguages extends CompiledBase
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function finalizeObject()
|
||||
{
|
||||
@@ -44,6 +55,8 @@ class CompiledLanguages extends CompiledBase
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modified()
|
||||
{
|
||||
@@ -55,6 +68,7 @@ class CompiledLanguages extends CompiledBase
|
||||
*
|
||||
* @param string $name Name of the position.
|
||||
* @param string $filename File to be loaded.
|
||||
* @return void
|
||||
*/
|
||||
protected function loadFile($name, $filename)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -13,19 +14,42 @@ use Grav\Common\Grav;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Service\ConfigServiceProvider;
|
||||
use Grav\Common\Utils;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class Config extends Data
|
||||
{
|
||||
/** @var string */
|
||||
protected $checksum;
|
||||
protected $modified = false;
|
||||
protected $timestamp = 0;
|
||||
public $environment;
|
||||
|
||||
/** @var string */
|
||||
protected $key;
|
||||
/** @var string */
|
||||
protected $checksum;
|
||||
/** @var int */
|
||||
protected $timestamp = 0;
|
||||
/** @var bool */
|
||||
protected $modified = false;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->checksum();
|
||||
if (null === $this->key) {
|
||||
$this->key = md5($this->checksum . $this->timestamp);
|
||||
}
|
||||
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $checksum
|
||||
* @return string|null
|
||||
*/
|
||||
public function checksum($checksum = null)
|
||||
{
|
||||
if ($checksum !== null) {
|
||||
@@ -35,6 +59,10 @@ class Config extends Data
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|null $modified
|
||||
* @return bool
|
||||
*/
|
||||
public function modified($modified = null)
|
||||
{
|
||||
if ($modified !== null) {
|
||||
@@ -44,6 +72,10 @@ class Config extends Data
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $timestamp
|
||||
* @return int
|
||||
*/
|
||||
public function timestamp($timestamp = null)
|
||||
{
|
||||
if ($timestamp !== null) {
|
||||
@@ -53,6 +85,9 @@ class Config extends Data
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function reload()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
@@ -75,6 +110,9 @@ class Config extends Data
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function debug()
|
||||
{
|
||||
/** @var Debugger $debugger */
|
||||
@@ -86,6 +124,9 @@ class Config extends Data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$setup = Grav::instance()['setup']->toArray();
|
||||
@@ -98,14 +139,13 @@ class Config extends Data
|
||||
}
|
||||
}
|
||||
|
||||
// Override the media.upload_limit based on PHP values
|
||||
$upload_limit = Utils::getUploadLimit();
|
||||
$this->items['system']['media']['upload_limit'] = $upload_limit > 0 ? $upload_limit : 1024*1024*1024;
|
||||
// Legacy value - Override the media.upload_limit based on PHP values
|
||||
$this->items['system']['media']['upload_limit'] = Utils::getUploadLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @deprecated
|
||||
* @deprecated 1.5 Use Grav::instance()['languages'] instead.
|
||||
*/
|
||||
public function getLanguages()
|
||||
{
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use RecursiveDirectoryIterator;
|
||||
|
||||
/**
|
||||
* Class ConfigFileFinder
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class ConfigFileFinder
|
||||
{
|
||||
/** @var string */
|
||||
protected $base = '';
|
||||
|
||||
/**
|
||||
@@ -39,6 +47,7 @@ class ConfigFileFinder
|
||||
foreach ($paths as $folder) {
|
||||
$list += $this->detectRecursive($folder, $pattern, $levels);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -60,6 +69,7 @@ class ConfigFileFinder
|
||||
|
||||
$list += $files[trim($path, '/')];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -77,6 +87,7 @@ class ConfigFileFinder
|
||||
foreach ($paths as $folder) {
|
||||
$list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels));
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -95,6 +106,7 @@ class ConfigFileFinder
|
||||
foreach ($folders as $folder) {
|
||||
$list += $this->detectInFolder($folder, $filename);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -102,7 +114,7 @@ class ConfigFileFinder
|
||||
* Find filename from a list of folders.
|
||||
*
|
||||
* @param array $folders
|
||||
* @param string $filename
|
||||
* @param string|null $filename
|
||||
* @return array
|
||||
*/
|
||||
public function locateInFolders(array $folders, $filename = null)
|
||||
@@ -112,6 +124,7 @@ class ConfigFileFinder
|
||||
$path = trim(Folder::getRelativePath($folder), '/');
|
||||
$list[$path] = $this->detectInFolder($folder, $filename);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -165,7 +178,7 @@ class ConfigFileFinder
|
||||
'filters' => [
|
||||
'pre-key' => $this->base,
|
||||
'key' => $pattern,
|
||||
'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
|
||||
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
|
||||
return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
|
||||
}
|
||||
],
|
||||
@@ -186,7 +199,7 @@ class ConfigFileFinder
|
||||
* Detects all directories with the lookup file and returns them with last modification time.
|
||||
*
|
||||
* @param string $folder Location to look up from.
|
||||
* @param string $lookup Filename to be located (defaults to directory name).
|
||||
* @param string|null $lookup Filename to be located (defaults to directory name).
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
@@ -199,9 +212,7 @@ class ConfigFileFinder
|
||||
$list = [];
|
||||
|
||||
if (is_dir($folder)) {
|
||||
$iterator = new \DirectoryIterator($folder);
|
||||
|
||||
/** @var \DirectoryIterator $directory */
|
||||
$iterator = new DirectoryIterator($folder);
|
||||
foreach ($iterator as $directory) {
|
||||
if (!$directory->isDir() || $directory->isDot()) {
|
||||
continue;
|
||||
@@ -243,7 +254,7 @@ class ConfigFileFinder
|
||||
'filters' => [
|
||||
'pre-key' => $this->base,
|
||||
'key' => $pattern,
|
||||
'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
|
||||
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
|
||||
return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()];
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -11,8 +12,25 @@ namespace Grav\Common\Config;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Class Languages
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class Languages extends Data
|
||||
{
|
||||
/** @var string|null */
|
||||
protected $checksum;
|
||||
|
||||
/** @var bool */
|
||||
protected $modified = false;
|
||||
|
||||
/** @var int */
|
||||
protected $timestamp = 0;
|
||||
|
||||
/**
|
||||
* @param string|null $checksum
|
||||
* @return string|null
|
||||
*/
|
||||
public function checksum($checksum = null)
|
||||
{
|
||||
if ($checksum !== null) {
|
||||
@@ -22,6 +40,10 @@ class Languages extends Data
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|null $modified
|
||||
* @return bool
|
||||
*/
|
||||
public function modified($modified = null)
|
||||
{
|
||||
if ($modified !== null) {
|
||||
@@ -31,6 +53,10 @@ class Languages extends Data
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $timestamp
|
||||
* @return int
|
||||
*/
|
||||
public function timestamp($timestamp = null)
|
||||
{
|
||||
if ($timestamp !== null) {
|
||||
@@ -40,6 +66,9 @@ class Languages extends Data
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reformat()
|
||||
{
|
||||
if (isset($this->items['plugins'])) {
|
||||
@@ -48,8 +77,31 @@ class Languages extends Data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function mergeRecursive(array $data)
|
||||
{
|
||||
$this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $lang
|
||||
* @return array
|
||||
*/
|
||||
public function flattenByLang($lang)
|
||||
{
|
||||
$language = $this->items[$lang];
|
||||
return Utils::arrayFlattenDotNotation($language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public function unflatten($array)
|
||||
{
|
||||
return Utils::arrayUnflattenDotNotation($array);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Config
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Utils;
|
||||
use InvalidArgumentException;
|
||||
use Pimple\Container;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function defined;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class Setup
|
||||
* @package Grav\Common\Config
|
||||
*/
|
||||
class Setup extends Data
|
||||
{
|
||||
/**
|
||||
* @var array Environment aliases normalized to lower case.
|
||||
*/
|
||||
public static $environments = [
|
||||
'' => 'unknown',
|
||||
'127.0.0.1' => 'localhost',
|
||||
'::1' => 'localhost'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string|null Current environment normalized to lower case.
|
||||
*/
|
||||
public static $environment;
|
||||
|
||||
/** @var array */
|
||||
protected $streams = [
|
||||
'system' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['system'],
|
||||
]
|
||||
],
|
||||
'user' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['user'],
|
||||
'' => [] // Set in constructor
|
||||
]
|
||||
],
|
||||
'cache' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => [], // Set in constructor
|
||||
'images' => ['images']
|
||||
]
|
||||
],
|
||||
'log' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => [] // Set in constructor
|
||||
]
|
||||
],
|
||||
'tmp' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => [] // Set in constructor
|
||||
]
|
||||
],
|
||||
'backup' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => [] // Set in constructor
|
||||
]
|
||||
],
|
||||
'environment' => [
|
||||
'type' => 'ReadOnlyStream'
|
||||
// If not defined, environment will be set up in the constructor.
|
||||
],
|
||||
'asset' => [
|
||||
'system' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['system'],
|
||||
]
|
||||
],
|
||||
'asset' => [
|
||||
'type' => 'Stream',
|
||||
'prefixes' => [
|
||||
'' => ['assets'],
|
||||
]
|
||||
@@ -46,13 +98,13 @@ class Setup extends Data
|
||||
'blueprints' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['environment://blueprints', 'user://blueprints', 'system/blueprints'],
|
||||
'' => ['environment://blueprints', 'user://blueprints', 'system://blueprints'],
|
||||
]
|
||||
],
|
||||
'config' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['environment://config', 'user://config', 'system/config'],
|
||||
'' => ['environment://config', 'user://config', 'system://config'],
|
||||
]
|
||||
],
|
||||
'plugins' => [
|
||||
@@ -76,40 +128,11 @@ class Setup extends Data
|
||||
'languages' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['environment://languages', 'user://languages', 'system/languages'],
|
||||
]
|
||||
],
|
||||
'cache' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['cache'],
|
||||
'images' => ['images']
|
||||
]
|
||||
],
|
||||
'log' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['logs']
|
||||
]
|
||||
],
|
||||
'backup' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['backup']
|
||||
]
|
||||
],
|
||||
'tmp' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['tmp']
|
||||
'' => ['environment://languages', 'user://languages', 'system://languages'],
|
||||
]
|
||||
],
|
||||
'image' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'type' => 'Stream',
|
||||
'prefixes' => [
|
||||
'' => ['user://images', 'system://images']
|
||||
]
|
||||
@@ -120,6 +143,13 @@ class Setup extends Data
|
||||
'' => ['user://pages']
|
||||
]
|
||||
],
|
||||
'user-data' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['user://data']
|
||||
]
|
||||
],
|
||||
'account' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
@@ -133,13 +163,58 @@ class Setup extends Data
|
||||
*/
|
||||
public function __construct($container)
|
||||
{
|
||||
$environment = null !== static::$environment ? static::$environment : ($container['uri']->environment() ?: 'localhost');
|
||||
// Configure main streams.
|
||||
$abs = str_starts_with(GRAV_SYSTEM_PATH, '/');
|
||||
$this->streams['system']['prefixes'][''] = $abs ? ['system', GRAV_SYSTEM_PATH] : ['system'];
|
||||
$this->streams['user']['prefixes'][''] = [GRAV_USER_PATH];
|
||||
$this->streams['cache']['prefixes'][''] = [GRAV_CACHE_PATH];
|
||||
$this->streams['log']['prefixes'][''] = [GRAV_LOG_PATH];
|
||||
$this->streams['tmp']['prefixes'][''] = [GRAV_TMP_PATH];
|
||||
$this->streams['backup']['prefixes'][''] = [GRAV_BACKUP_PATH];
|
||||
|
||||
// If environment is not set, look for the environment variable and then the constant.
|
||||
$environment = static::$environment ??
|
||||
(defined('GRAV_ENVIRONMENT') ? GRAV_ENVIRONMENT : (getenv('GRAV_ENVIRONMENT') ?: null));
|
||||
|
||||
// If no environment is set, make sure we get one (CLI or hostname).
|
||||
if (null === $environment) {
|
||||
if (defined('GRAV_CLI')) {
|
||||
$environment = 'cli';
|
||||
} else {
|
||||
/** @var ServerRequestInterface $request */
|
||||
$request = $container['request'];
|
||||
$host = $request->getUri()->getHost();
|
||||
|
||||
$environment = Utils::substrToString($host, ':');
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve server aliases to the proper environment.
|
||||
static::$environment = static::$environments[$environment] ?? $environment;
|
||||
|
||||
// Pre-load setup.php which contains our initial configuration.
|
||||
// Configuration may contain dynamic parts, which is why we need to always load it.
|
||||
// If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults.
|
||||
$file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php';
|
||||
$setup = is_file($file) ? (array) include $file : [];
|
||||
// If GRAV_SETUP_PATH has been defined, use it, otherwise use defaults.
|
||||
$setupFile = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : (getenv('GRAV_SETUP_PATH') ?: null);
|
||||
if (null !== $setupFile) {
|
||||
// Make sure that the custom setup file exists. Terminates the script if not.
|
||||
if (!str_starts_with($setupFile, '/')) {
|
||||
$setupFile = GRAV_WEBROOT . '/' . $setupFile;
|
||||
}
|
||||
if (!is_file($setupFile)) {
|
||||
echo 'GRAV_SETUP_PATH is defined but does not point to existing setup file.';
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
$setupFile = GRAV_WEBROOT . '/setup.php';
|
||||
if (!is_file($setupFile)) {
|
||||
$setupFile = GRAV_WEBROOT . '/' . GRAV_USER_PATH . '/setup.php';
|
||||
}
|
||||
if (!is_file($setupFile)) {
|
||||
$setupFile = null;
|
||||
}
|
||||
}
|
||||
$setup = $setupFile ? (array) include $setupFile : [];
|
||||
|
||||
// Add default streams defined in beginning of the class.
|
||||
if (!isset($setup['streams']['schemes'])) {
|
||||
@@ -150,19 +225,41 @@ class Setup extends Data
|
||||
// Initialize class.
|
||||
parent::__construct($setup);
|
||||
|
||||
$this->def('environment', static::$environment);
|
||||
|
||||
// Figure out path for the current environment.
|
||||
$envPath = defined('GRAV_ENVIRONMENT_PATH') ? GRAV_ENVIRONMENT_PATH : (getenv('GRAV_ENVIRONMENT_PATH') ?: null);
|
||||
if (null === $envPath) {
|
||||
// Find common path for all environments and append current environment into it.
|
||||
$envPath = defined('GRAV_ENVIRONMENTS_PATH') ? GRAV_ENVIRONMENTS_PATH : (getenv('GRAV_ENVIRONMENTS_PATH') ?: null);
|
||||
if (null !== $envPath) {
|
||||
$envPath .= '/';
|
||||
} else {
|
||||
// Use default location. Start with Grav 1.7 default.
|
||||
$envPath = GRAV_WEBROOT. '/' . GRAV_USER_PATH . '/env';
|
||||
if (is_dir($envPath)) {
|
||||
$envPath = 'user://env/';
|
||||
} else {
|
||||
// Fallback to Grav 1.6 default.
|
||||
$envPath = 'user://';
|
||||
}
|
||||
}
|
||||
$envPath .= $this->get('environment');
|
||||
}
|
||||
|
||||
// Set up environment.
|
||||
$this->def('environment', $environment ?: 'cli');
|
||||
$this->def('streams.schemes.environment.prefixes', ['' => $environment ? ["user://{$this->environment}"] : []]);
|
||||
$this->def('environment', static::$environment);
|
||||
$this->def('streams.schemes.environment.prefixes', ['' => [$envPath]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
* @throws \RuntimeException
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$locator = new UniformResourceLocator(GRAV_ROOT);
|
||||
$locator = new UniformResourceLocator(GRAV_WEBROOT);
|
||||
$files = [];
|
||||
|
||||
$guard = 5;
|
||||
@@ -186,7 +283,7 @@ class Setup extends Data
|
||||
} while (--$guard);
|
||||
|
||||
if (!$guard) {
|
||||
throw new \RuntimeException('Setup: Configuration reload loop detected!');
|
||||
throw new RuntimeException('Setup: Configuration reload loop detected!');
|
||||
}
|
||||
|
||||
// Make sure we have valid setup.
|
||||
@@ -199,7 +296,8 @@ class Setup extends Data
|
||||
* Initialize resource locator by using the configuration.
|
||||
*
|
||||
* @param UniformResourceLocator $locator
|
||||
* @throws \BadMethodCallException
|
||||
* @return void
|
||||
* @throws BadMethodCallException
|
||||
*/
|
||||
public function initializeLocator(UniformResourceLocator $locator)
|
||||
{
|
||||
@@ -212,8 +310,8 @@ class Setup extends Data
|
||||
$locator->addPath($scheme, '', $config['paths']);
|
||||
}
|
||||
|
||||
$override = isset($config['override']) ? $config['override'] : false;
|
||||
$force = isset($config['force']) ? $config['force'] : false;
|
||||
$override = $config['override'] ?? false;
|
||||
$force = $config['force'] ?? false;
|
||||
|
||||
if (isset($config['prefixes'])) {
|
||||
foreach ((array)$config['prefixes'] as $prefix => $paths) {
|
||||
@@ -232,7 +330,7 @@ class Setup extends Data
|
||||
{
|
||||
$schemes = [];
|
||||
foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
|
||||
$type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
|
||||
$type = $config['type'] ?? 'ReadOnlyStream';
|
||||
if ($type[0] !== '\\') {
|
||||
$type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
|
||||
}
|
||||
@@ -245,24 +343,51 @@ class Setup extends Data
|
||||
|
||||
/**
|
||||
* @param UniformResourceLocator $locator
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \BadMethodCallException
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
* @throws BadMethodCallException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function check(UniformResourceLocator $locator)
|
||||
{
|
||||
$streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null;
|
||||
$streams = $this->items['streams']['schemes'] ?? null;
|
||||
if (!is_array($streams)) {
|
||||
throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
|
||||
throw new InvalidArgumentException('Configuration is missing streams.schemes!');
|
||||
}
|
||||
$diff = array_keys(array_diff_key($this->streams, $streams));
|
||||
if ($diff) {
|
||||
throw new \InvalidArgumentException(
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff))
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// If environment is found, remove all missing override locations (B/C compatibility).
|
||||
if ($locator->findResource('environment://', true)) {
|
||||
$force = $this->get('streams.schemes.environment.force', false);
|
||||
if (!$force) {
|
||||
$prefixes = $this->get('streams.schemes.environment.prefixes.');
|
||||
$update = false;
|
||||
foreach ($prefixes as $i => $prefix) {
|
||||
if ($locator->isStream($prefix)) {
|
||||
if ($locator->findResource($prefix, true)) {
|
||||
break;
|
||||
}
|
||||
} elseif (file_exists($prefix)) {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($prefixes[$i]);
|
||||
$update = true;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$this->set('streams.schemes.environment.prefixes', ['' => array_values($prefixes)]);
|
||||
$this->initializeLocator($locator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$locator->findResource('environment://config', true)) {
|
||||
// If environment does not have its own directory, remove it from the lookup.
|
||||
$this->set('streams.schemes.environment.prefixes', ['config' => []]);
|
||||
@@ -271,13 +396,17 @@ class Setup extends Data
|
||||
|
||||
// Create security.yaml if it doesn't exist.
|
||||
$filename = $locator->findResource('config://security.yaml', true, true);
|
||||
$file = YamlFile::instance($filename);
|
||||
if (!$file->exists()) {
|
||||
$file->save(['salt' => Utils::generateRandomString(14)]);
|
||||
$file->free();
|
||||
$security_file = CompiledYamlFile::instance($filename);
|
||||
$security_content = (array)$security_file->content();
|
||||
|
||||
if (!isset($security_content['salt'])) {
|
||||
$security_content = array_merge($security_content, ['salt' => Utils::generateRandomString(14)]);
|
||||
$security_file->content($security_content);
|
||||
$security_file->save();
|
||||
$security_file->free();
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,21 +11,72 @@ namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use RocketTheme\Toolbox\Blueprints\BlueprintForm;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function call_user_func_array;
|
||||
use function count;
|
||||
use function function_exists;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Blueprint
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
class Blueprint extends BlueprintForm
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $context = 'blueprints://';
|
||||
|
||||
/**
|
||||
* @var BlueprintSchema
|
||||
*/
|
||||
/** @var string|null */
|
||||
protected $scope;
|
||||
|
||||
/** @var BlueprintSchema */
|
||||
protected $blueprintSchema;
|
||||
|
||||
/** @var object|null */
|
||||
protected $object;
|
||||
|
||||
/** @var array|null */
|
||||
protected $defaults;
|
||||
|
||||
/** @var array */
|
||||
protected $handlers = [];
|
||||
|
||||
/**
|
||||
* Clone blueprint.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->blueprintSchema) {
|
||||
$this->blueprintSchema = clone $this->blueprintSchema;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $scope
|
||||
* @return void
|
||||
*/
|
||||
public function setScope($scope)
|
||||
{
|
||||
$this->scope = $scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
* @return void
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->object = $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values for field types.
|
||||
*
|
||||
@@ -40,6 +92,29 @@ class Blueprint extends BlueprintForm
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array|mixed|null
|
||||
* @since 1.7
|
||||
*/
|
||||
public function getDefaultValue(string $name)
|
||||
{
|
||||
$path = explode('.', $name) ?: [];
|
||||
$current = $this->getDefaults();
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (is_object($current) && isset($current->{$field})) {
|
||||
$current = $current->{$field};
|
||||
} elseif (is_array($current) && isset($current[$field])) {
|
||||
$current = $current[$field];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested structure containing default values defined in the blueprints.
|
||||
*
|
||||
@@ -51,7 +126,93 @@ class Blueprint extends BlueprintForm
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema->getDefaults();
|
||||
if (null === $this->defaults) {
|
||||
$this->defaults = $this->blueprintSchema->getDefaults();
|
||||
}
|
||||
|
||||
return $this->defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize blueprints with its dynamic fields.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
foreach ($this->dynamic as $key => $data) {
|
||||
// Locate field.
|
||||
$path = explode('/', $key);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (is_object($current)) {
|
||||
// Handle objects.
|
||||
if (!isset($current->{$field})) {
|
||||
$current->{$field} = [];
|
||||
}
|
||||
|
||||
$current = &$current->{$field};
|
||||
} else {
|
||||
// Handle arrays and scalars.
|
||||
if (!is_array($current)) {
|
||||
$current = [$field => []];
|
||||
} elseif (!isset($current[$field])) {
|
||||
$current[$field] = [];
|
||||
}
|
||||
|
||||
$current = &$current[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// Set dynamic property.
|
||||
foreach ($data as $property => $call) {
|
||||
$action = $call['action'];
|
||||
$method = 'dynamic' . ucfirst($action);
|
||||
$call['object'] = $this->object;
|
||||
|
||||
if (isset($this->handlers[$action])) {
|
||||
$callable = $this->handlers[$action];
|
||||
$callable($current, $property, $call);
|
||||
} elseif (method_exists($this, $method)) {
|
||||
$this->{$method}($current, $property, $call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend blueprint with another blueprint.
|
||||
*
|
||||
* @param BlueprintForm|array $extends
|
||||
* @param bool $append
|
||||
* @return $this
|
||||
*/
|
||||
public function extend($extends, $append = false)
|
||||
{
|
||||
parent::extend($extends, $append);
|
||||
|
||||
$this->deepInit($this->items);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @param string $separator
|
||||
* @param bool $append
|
||||
* @return $this
|
||||
*/
|
||||
public function embed($name, $value, $separator = '/', $append = false)
|
||||
{
|
||||
parent::embed($name, $value, $separator, $append);
|
||||
|
||||
$this->deepInit($this->items);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +220,7 @@ class Blueprint extends BlueprintForm
|
||||
*
|
||||
* @param array $data1
|
||||
* @param array $data2
|
||||
* @param string $name Optional
|
||||
* @param string|null $name Optional
|
||||
* @param string $separator Optional
|
||||
* @return array
|
||||
*/
|
||||
@@ -70,6 +231,20 @@ class Blueprint extends BlueprintForm
|
||||
return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data coming from a form.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $toggles
|
||||
* @return array
|
||||
*/
|
||||
public function processForm(array $data, array $toggles = [])
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema->processForm($data, $toggles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data fields that do not exist in blueprints.
|
||||
*
|
||||
@@ -88,28 +263,48 @@ class Blueprint extends BlueprintForm
|
||||
* Validate data against blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @throws \RuntimeException
|
||||
* @param array $options
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
public function validate(array $data, array $options = [])
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
$this->blueprintSchema->validate($data);
|
||||
$this->blueprintSchema->validate($data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data by using blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param bool $missingValuesAsNull
|
||||
* @param bool $keepEmptyValues
|
||||
* @return array
|
||||
*/
|
||||
public function filter(array $data)
|
||||
public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema->filter($data);
|
||||
return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues) ?? [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flatten data by using blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param bool $includeAll
|
||||
* @return array
|
||||
*/
|
||||
public function flattenData(array $data, bool $includeAll = false)
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema->flattenData($data, $includeAll);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return blueprint data schema.
|
||||
*
|
||||
@@ -122,31 +317,46 @@ class Blueprint extends BlueprintForm
|
||||
return $this->blueprintSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param callable $callable
|
||||
* @return void
|
||||
*/
|
||||
public function addDynamicHandler(string $name, callable $callable): void
|
||||
{
|
||||
$this->handlers[$name] = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize validator.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initInternals()
|
||||
{
|
||||
if (!isset($this->blueprintSchema)) {
|
||||
if (null === $this->blueprintSchema) {
|
||||
$types = Grav::instance()['plugins']->formFieldTypes;
|
||||
|
||||
$this->blueprintSchema = new BlueprintSchema;
|
||||
|
||||
if ($types) {
|
||||
$this->blueprintSchema->setTypes($types);
|
||||
}
|
||||
|
||||
$this->blueprintSchema->embed('', $this->items);
|
||||
$this->blueprintSchema->init();
|
||||
$this->defaults = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
protected function loadFile($filename)
|
||||
{
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$content = $file->content();
|
||||
$content = (array)$file->content();
|
||||
$file->free();
|
||||
|
||||
return $content;
|
||||
@@ -154,7 +364,7 @@ class Blueprint extends BlueprintForm
|
||||
|
||||
/**
|
||||
* @param string|array $path
|
||||
* @param string $context
|
||||
* @param string|null $context
|
||||
* @return array
|
||||
*/
|
||||
protected function getFiles($path, $context = null)
|
||||
@@ -163,16 +373,26 @@ class Blueprint extends BlueprintForm
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if (is_string($path) && !$locator->isStream($path)) {
|
||||
if (is_file($path)) {
|
||||
return [$path];
|
||||
}
|
||||
|
||||
// Find path overrides.
|
||||
$paths = isset($this->overrides[$path]) ? (array) $this->overrides[$path] : [];
|
||||
if (null === $context) {
|
||||
$paths = (array) ($this->overrides[$path] ?? null);
|
||||
} else {
|
||||
$paths = [];
|
||||
}
|
||||
|
||||
// Add path pointing to default context.
|
||||
if ($context === null) {
|
||||
$context = $this->context;
|
||||
}
|
||||
|
||||
if ($context && $context[strlen($context)-1] !== '/') {
|
||||
$context .= '/';
|
||||
}
|
||||
|
||||
$path = $context . $path;
|
||||
|
||||
if (!preg_match('/\.yaml$/', $path)) {
|
||||
@@ -200,6 +420,7 @@ class Blueprint extends BlueprintForm
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
* @return void
|
||||
*/
|
||||
protected function dynamicData(array &$field, $property, array &$call)
|
||||
{
|
||||
@@ -212,20 +433,22 @@ class Blueprint extends BlueprintForm
|
||||
$params = [];
|
||||
}
|
||||
|
||||
list($o, $f) = preg_split('/::/', $function, 2);
|
||||
[$o, $f] = explode('::', $function, 2);
|
||||
|
||||
$data = null;
|
||||
if (!$f) {
|
||||
if (function_exists($o)) {
|
||||
$data = call_user_func_array($o, $params);
|
||||
}
|
||||
} else {
|
||||
if (method_exists($o, $f)) {
|
||||
$data = call_user_func_array(array($o, $f), $params);
|
||||
$data = call_user_func_array([$o, $f], $params);
|
||||
}
|
||||
}
|
||||
|
||||
// If function returns a value,
|
||||
if (isset($data)) {
|
||||
if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
|
||||
if (null !== $data) {
|
||||
if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
|
||||
// Combine field and @data-field together.
|
||||
$field[$property] += $data;
|
||||
} else {
|
||||
@@ -239,16 +462,132 @@ class Blueprint extends BlueprintForm
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
* @return void
|
||||
*/
|
||||
protected function dynamicConfig(array &$field, $property, array &$call)
|
||||
{
|
||||
$value = $call['params'];
|
||||
$params = $call['params'];
|
||||
if (is_array($params)) {
|
||||
$value = array_shift($params);
|
||||
$params = array_shift($params);
|
||||
} else {
|
||||
$value = $params;
|
||||
$params = [];
|
||||
}
|
||||
|
||||
$default = isset($field[$property]) ? $field[$property] : null;
|
||||
$default = $field[$property] ?? null;
|
||||
$config = Grav::instance()['config']->get($value, $default);
|
||||
if (!empty($field['value_only'])) {
|
||||
$config = array_combine($config, $config);
|
||||
}
|
||||
|
||||
if (!is_null($config)) {
|
||||
$field[$property] = $config;
|
||||
if (null !== $config) {
|
||||
if (!empty($params['append']) && is_array($config) && isset($field[$property]) && is_array($field[$property])) {
|
||||
// Combine field and @config-field together.
|
||||
$field[$property] += $config;
|
||||
} else {
|
||||
// Or create/replace field with @config-field.
|
||||
$field[$property] = $config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
* @return void
|
||||
*/
|
||||
protected function dynamicSecurity(array &$field, $property, array &$call)
|
||||
{
|
||||
if ($property || !empty($field['validate']['ignore'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
$actions = (array)$call['params'];
|
||||
|
||||
/** @var UserInterface|null $user */
|
||||
$user = $grav['user'] ?? null;
|
||||
$success = null !== $user;
|
||||
if ($success) {
|
||||
$success = $this->resolveActions($user, $actions);
|
||||
}
|
||||
if (!$success) {
|
||||
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface|null $user
|
||||
* @param array $actions
|
||||
* @param string $op
|
||||
* @return bool
|
||||
*/
|
||||
protected function resolveActions(?UserInterface $user, array $actions, string $op = 'and')
|
||||
{
|
||||
if (null === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$c = $i = count($actions);
|
||||
foreach ($actions as $key => $action) {
|
||||
if (!is_int($key) && is_array($actions)) {
|
||||
$i -= $this->resolveActions($user, $action, $key);
|
||||
} elseif ($user->authorize($action)) {
|
||||
$i--;
|
||||
}
|
||||
}
|
||||
|
||||
if ($op === 'and') {
|
||||
return $i === 0;
|
||||
}
|
||||
|
||||
return $c !== $i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
* @return void
|
||||
*/
|
||||
protected function dynamicScope(array &$field, $property, array &$call)
|
||||
{
|
||||
if ($property && $property !== 'ignore') {
|
||||
return;
|
||||
}
|
||||
|
||||
$scopes = (array)$call['params'];
|
||||
$matches = in_array($this->scope, $scopes, true);
|
||||
if ($this->scope && $property !== 'ignore') {
|
||||
$matches = !$matches;
|
||||
}
|
||||
|
||||
if ($matches) {
|
||||
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
protected function addPropertyRecursive(array &$field, $property, $value)
|
||||
{
|
||||
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
|
||||
$field[$property] = array_merge_recursive($field[$property], $value);
|
||||
} else {
|
||||
$field[$property] = $value;
|
||||
}
|
||||
|
||||
if (!empty($field['fields'])) {
|
||||
foreach ($field['fields'] as $key => &$child) {
|
||||
$this->addPropertyRecursive($child, $property, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\Blueprints\BlueprintSchema as BlueprintSchemaBase;
|
||||
use RuntimeException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class BlueprintSchema
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
{
|
||||
use Export;
|
||||
|
||||
/** @var array */
|
||||
protected $filter = ['validation' => true, 'xss_check' => true];
|
||||
|
||||
/** @var array */
|
||||
protected $ignoreFormKeys = [
|
||||
'title' => true,
|
||||
'help' => true,
|
||||
@@ -26,18 +39,37 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
'fields' => true
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTypes()
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public function getType($name)
|
||||
{
|
||||
return $this->types[$name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data against blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @throws \RuntimeException
|
||||
* @param array $options
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
public function validate(array $data, array $options = [])
|
||||
{
|
||||
try {
|
||||
$messages = $this->validateArray($data, $this->nested);
|
||||
|
||||
} catch (\RuntimeException $e) {
|
||||
$validation = $this->items['']['form']['validation'] ?? 'loose';
|
||||
$messages = $this->validateArray($data, $this->nested, $validation === 'strict', $options['xss_check'] ?? true);
|
||||
} catch (RuntimeException $e) {
|
||||
throw (new ValidationException($e->getMessage(), $e->getCode(), $e))->setMessages();
|
||||
}
|
||||
|
||||
@@ -47,40 +79,125 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data by using blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $data
|
||||
* @param array $toggles
|
||||
* @return array
|
||||
*/
|
||||
public function filter(array $data)
|
||||
public function processForm(array $data, array $toggles = [])
|
||||
{
|
||||
return $this->filterArray($data, $this->nested);
|
||||
return $this->processFormRecursive($data, $toggles, $this->nested) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data by using blueprints.
|
||||
*
|
||||
* @param array $data Incoming data, for example from a form.
|
||||
* @param bool $missingValuesAsNull Include missing values as nulls.
|
||||
* @param bool $keepEmptyValues Include empty values.
|
||||
* @return array
|
||||
*/
|
||||
public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false)
|
||||
{
|
||||
$this->buildIgnoreNested($this->nested);
|
||||
|
||||
return $this->filterArray($data, $this->nested, '', $missingValuesAsNull, $keepEmptyValues) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten data by using blueprints.
|
||||
*
|
||||
* @param array $data Data to be flattened.
|
||||
* @param bool $includeAll
|
||||
* @return array
|
||||
*/
|
||||
public function flattenData(array $data, bool $includeAll = false)
|
||||
{
|
||||
$list = [];
|
||||
if ($includeAll) {
|
||||
foreach ($this->items as $key => $rules) {
|
||||
$type = $rules['type'] ?? '';
|
||||
if (!str_starts_with($type, '_') && !str_contains($key, '*')) {
|
||||
$list[$key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_replace($list, $this->flattenArray($data, $this->nested, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @returns array
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
protected function validateArray(array $data, array $rules)
|
||||
protected function flattenArray(array $data, array $rules, string $prefix)
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = is_string($val) ? $this->items[$val] : null;
|
||||
|
||||
if ($rule || isset($val['*'])) {
|
||||
// Item has been defined in blueprints.
|
||||
$array[$prefix.$key] = $field;
|
||||
} elseif (is_array($field) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$array += $this->flattenArray($field, $val, $prefix . $key . '.');
|
||||
} else {
|
||||
// Undefined/extra item.
|
||||
$array[$prefix.$key] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @param bool $strict
|
||||
* @param bool $xss
|
||||
* @return array
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function validateArray(array $data, array $rules, bool $strict, bool $xss = true)
|
||||
{
|
||||
$messages = $this->checkRequired($data, $rules);
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
$val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
|
||||
foreach ($data as $key => $child) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = is_string($val) ? $this->items[$val] : null;
|
||||
$checkXss = $xss;
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
$messages += Validation::validate($field, $rule);
|
||||
} elseif (is_array($field) && is_array($val)) {
|
||||
if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
|
||||
// Skip validation in the ignored field.
|
||||
continue;
|
||||
}
|
||||
|
||||
$messages += Validation::validate($child, $rule);
|
||||
|
||||
} elseif (is_array($child) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$messages += $this->validateArray($field, $val);
|
||||
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
|
||||
// Undefined/extra item.
|
||||
throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
|
||||
$messages += $this->validateArray($child, $val, $strict);
|
||||
$checkXss = false;
|
||||
|
||||
} elseif ($strict) {
|
||||
// Undefined/extra item in strict mode.
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) {
|
||||
throw new RuntimeException(sprintf('%s is not defined in blueprints', $key));
|
||||
}
|
||||
|
||||
user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if ($checkXss) {
|
||||
$messages += Validation::checkSafety($child, $rule ?: ['name' => $key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,32 +207,139 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @return array
|
||||
* @internal
|
||||
* @param string $parent
|
||||
* @param bool $missingValuesAsNull
|
||||
* @param bool $keepEmptyValues
|
||||
* @return array|null
|
||||
*/
|
||||
protected function filterArray(array $data, array $rules)
|
||||
protected function filterArray(array $data, array $rules, string $parent, bool $missingValuesAsNull, bool $keepEmptyValues)
|
||||
{
|
||||
$results = array();
|
||||
foreach ($data as $key => $field) {
|
||||
$val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
|
||||
$rule = is_string($val) ? $this->items[$val] : null;
|
||||
$results = [];
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
foreach ($data as $key => $field) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = is_string($val) ? $this->items[$val] : $this->items[$parent . $key] ?? null;
|
||||
|
||||
if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
|
||||
// Skip any data in the ignored field.
|
||||
unset($results[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $field) {
|
||||
if ($missingValuesAsNull) {
|
||||
$results[$key] = null;
|
||||
} else {
|
||||
unset($results[$key]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$isParent = isset($val['*']);
|
||||
$type = $rule['type'] ?? null;
|
||||
|
||||
if (!$isParent && $type && $type !== '_parent') {
|
||||
$field = Validation::filter($field, $rule);
|
||||
} elseif (is_array($field) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$field = $this->filterArray($field, $val);
|
||||
$k = $isParent ? '*' : $key;
|
||||
$field = $this->filterArray($field, $val, $parent . $k . '.', $missingValuesAsNull, $keepEmptyValues);
|
||||
|
||||
if (null === $field) {
|
||||
// Nested parent has no values.
|
||||
unset($results[$key]);
|
||||
continue;
|
||||
}
|
||||
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
|
||||
$field = null;
|
||||
// Skip any extra data.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($field) && (!is_array($field) || !empty($field))) {
|
||||
if ($keepEmptyValues || (null !== $field && (!is_array($field) || !empty($field)))) {
|
||||
$results[$key] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
return $results ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $nested
|
||||
* @param string $parent
|
||||
* @return bool
|
||||
*/
|
||||
protected function buildIgnoreNested(array $nested, $parent = '')
|
||||
{
|
||||
$ignore = true;
|
||||
foreach ($nested as $key => $val) {
|
||||
$key = $parent . $key;
|
||||
if (is_array($val)) {
|
||||
$ignore = $this->buildIgnoreNested($val, $key . '.') && $ignore; // Keep the order!
|
||||
} else {
|
||||
$child = $this->items[$key] ?? null;
|
||||
$ignore = $ignore && (!$child || !empty($child['disabled']) || !empty($child['validate']['ignore']));
|
||||
}
|
||||
}
|
||||
if ($ignore) {
|
||||
$key = trim($parent, '.');
|
||||
$this->items[$key]['validate']['ignore'] = true;
|
||||
}
|
||||
|
||||
return $ignore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $data
|
||||
* @param array $toggles
|
||||
* @param array $nested
|
||||
* @return array|null
|
||||
*/
|
||||
protected function processFormRecursive(?array $data, array $toggles, array $nested)
|
||||
{
|
||||
foreach ($nested as $key => $value) {
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
if ($key === '*') {
|
||||
// TODO: Add support to collections.
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
// Special toggle handling for all the nested data.
|
||||
$toggle = $toggles[$key] ?? [];
|
||||
if (!is_array($toggle)) {
|
||||
if (!$toggle) {
|
||||
$data[$key] = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$toggle = [];
|
||||
}
|
||||
// Recursively fetch the items.
|
||||
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
|
||||
} else {
|
||||
$field = $this->get($value);
|
||||
// Do not add the field if:
|
||||
if (
|
||||
// Not an input field
|
||||
!$field
|
||||
// Field has been disabled
|
||||
|| !empty($field['disabled'])
|
||||
// Field validation is set to be ignored
|
||||
|| !empty($field['validate']['ignore'])
|
||||
// Field is overridable and the toggle is turned off
|
||||
|| (!empty($field['overridable']) && empty($toggles[$key]))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($data[$key])) {
|
||||
$data[$key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,10 +355,23 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
if (!is_string($field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field = $this->items[$field];
|
||||
|
||||
// Skip ignored field, it will not be required.
|
||||
if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip overridable fields without value.
|
||||
// TODO: We need better overridable support, which is not just ignoring required values but also looking if defaults are good.
|
||||
if (!empty($field['overridable']) && !isset($data[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if required.
|
||||
if (isset($field['validate']['required'])
|
||||
&& $field['validate']['required'] === true) {
|
||||
|
||||
if (isset($data[$name])) {
|
||||
continue;
|
||||
}
|
||||
@@ -142,9 +379,9 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = isset($field['label']) ? $field['label'] : $field['name'];
|
||||
$value = $field['label'] ?? $field['name'];
|
||||
$language = Grav::instance()['language'];
|
||||
$message = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
|
||||
$message = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
|
||||
$messages[$field['name']][] = $message;
|
||||
}
|
||||
}
|
||||
@@ -156,12 +393,13 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
* @return void
|
||||
*/
|
||||
protected function dynamicConfig(array &$field, $property, array &$call)
|
||||
{
|
||||
$value = $call['params'];
|
||||
|
||||
$default = isset($field[$property]) ? $field[$property] : null;
|
||||
$default = $field[$property] ?? null;
|
||||
$config = Grav::instance()['config']->get($value, $default);
|
||||
|
||||
if (null !== $config) {
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Class Blueprints
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
class Blueprints
|
||||
{
|
||||
/** @var array|string */
|
||||
protected $search;
|
||||
/** @var array */
|
||||
protected $types;
|
||||
/** @var array */
|
||||
protected $instances = [];
|
||||
|
||||
/**
|
||||
@@ -30,12 +42,13 @@ class Blueprints
|
||||
*
|
||||
* @param string $type Blueprint type.
|
||||
* @return Blueprint
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function get($type)
|
||||
{
|
||||
if (!isset($this->instances[$type])) {
|
||||
$this->instances[$type] = $this->loadFile($type);
|
||||
$blueprint = $this->loadFile($type);
|
||||
$this->instances[$type] = $blueprint;
|
||||
}
|
||||
|
||||
return $this->instances[$type];
|
||||
@@ -49,7 +62,7 @@ class Blueprints
|
||||
public function types()
|
||||
{
|
||||
if ($this->types === null) {
|
||||
$this->types = array();
|
||||
$this->types = [];
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
@@ -60,10 +73,9 @@ class Blueprints
|
||||
if ($locator->isStream($this->search)) {
|
||||
$iterator = $locator->getIterator($this->search);
|
||||
} else {
|
||||
$iterator = new \DirectoryIterator($this->search);
|
||||
$iterator = new DirectoryIterator($this->search);
|
||||
}
|
||||
|
||||
/** @var \DirectoryIterator $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || '.' . $file->getExtension() !== YAML_EXT) {
|
||||
continue;
|
||||
@@ -95,6 +107,15 @@ class Blueprints
|
||||
$blueprint->setContext($this->search);
|
||||
}
|
||||
|
||||
return $blueprint->load()->init();
|
||||
try {
|
||||
$blueprint->load()->init();
|
||||
} catch (RuntimeException $e) {
|
||||
$log = Grav::instance()['log'];
|
||||
$log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage()));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use ArrayAccess;
|
||||
use Exception;
|
||||
use JsonSerializable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Countable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
use RuntimeException;
|
||||
use function func_get_args;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function is_object;
|
||||
|
||||
class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
/**
|
||||
* Class Data
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable, ExportInterface
|
||||
{
|
||||
use NestedArrayAccessWithGetters, Countable, Export;
|
||||
|
||||
/** @var string */
|
||||
protected $gettersVariable = 'items';
|
||||
/** @var array */
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* @var Blueprints
|
||||
*/
|
||||
/** @var Blueprint|callable|null */
|
||||
protected $blueprints;
|
||||
|
||||
/**
|
||||
* @var File
|
||||
*/
|
||||
/** @var FileInterface|null */
|
||||
protected $storage;
|
||||
|
||||
/** @var bool */
|
||||
private $missingValuesAsNull = false;
|
||||
/** @var bool */
|
||||
private $keepEmptyValues = true;
|
||||
|
||||
/**
|
||||
* @param array $items
|
||||
* @param Blueprint|callable $blueprints
|
||||
* @param Blueprint|callable|null $blueprints
|
||||
*/
|
||||
public function __construct(array $items = array(), $blueprints = null)
|
||||
public function __construct(array $items = [], $blueprints = null)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->blueprints = $blueprints;
|
||||
if (null !== $blueprints) {
|
||||
$this->blueprints = $blueprints;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setKeepEmptyValues(bool $value)
|
||||
{
|
||||
$this->keepEmptyValues = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setMissingValuesAsNull(bool $value)
|
||||
{
|
||||
$this->missingValuesAsNull = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,20 +101,22 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
* @param mixed $value Value to be joined.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function join($name, $value, $separator = '.')
|
||||
{
|
||||
$old = $this->get($name, null, $separator);
|
||||
if ($old !== null) {
|
||||
if (!is_array($old)) {
|
||||
throw new \RuntimeException('Value ' . $old);
|
||||
throw new RuntimeException('Value ' . $old);
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
} elseif (!is_array($value)) {
|
||||
throw new \RuntimeException('Value ' . $value);
|
||||
throw new RuntimeException('Value ' . $value);
|
||||
}
|
||||
|
||||
$value = $this->blueprints()->mergeData($old, $value, $name, $separator);
|
||||
}
|
||||
|
||||
@@ -111,6 +150,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
$old = $this->get($name, null, $separator);
|
||||
if ($old !== null) {
|
||||
$value = $this->blueprints()->mergeData($value, $old, $name, $separator);
|
||||
@@ -125,17 +165,17 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
* Get value from the configuration and join it with given data.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param array $value Value to be joined.
|
||||
* @param array|object $value Value to be joined.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return array
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getJoined($name, $value, $separator = '.')
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
} elseif (!is_array($value)) {
|
||||
throw new \RuntimeException('Value ' . $value);
|
||||
throw new RuntimeException('Value ' . $value);
|
||||
}
|
||||
|
||||
$old = $this->get($name, null, $separator);
|
||||
@@ -146,7 +186,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
}
|
||||
|
||||
if (!is_array($old)) {
|
||||
throw new \RuntimeException('Value ' . $old);
|
||||
throw new RuntimeException('Value ' . $old);
|
||||
}
|
||||
|
||||
// Return joined data.
|
||||
@@ -184,7 +224,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
* Validate by blueprints.
|
||||
*
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
@@ -195,11 +235,14 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
* Filter all items by using blueprints.
|
||||
*/
|
||||
public function filter()
|
||||
{
|
||||
$this->items = $this->blueprints()->filter($this->items);
|
||||
$args = func_get_args();
|
||||
$missingValuesAsNull = (bool)(array_shift($args) ?? $this->missingValuesAsNull);
|
||||
$keepEmptyValues = (bool)(array_shift($args) ?? $this->keepEmptyValues);
|
||||
|
||||
$this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -221,19 +264,22 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
*/
|
||||
public function blueprints()
|
||||
{
|
||||
if (!$this->blueprints){
|
||||
$this->blueprints = new Blueprint;
|
||||
if (!$this->blueprints) {
|
||||
$this->blueprints = new Blueprint();
|
||||
} elseif (is_callable($this->blueprints)) {
|
||||
// Lazy load blueprints.
|
||||
$blueprints = $this->blueprints;
|
||||
$this->blueprints = $blueprints();
|
||||
}
|
||||
|
||||
return $this->blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data if storage has been defined.
|
||||
* @throws \RuntimeException
|
||||
*
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
@@ -274,14 +320,23 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
|
||||
/**
|
||||
* Set or get the data storage.
|
||||
*
|
||||
* @param FileInterface $storage Optionally enter a new storage.
|
||||
* @return FileInterface
|
||||
* @param FileInterface|null $storage Optionally enter a new storage.
|
||||
* @return FileInterface|null
|
||||
*/
|
||||
public function file(FileInterface $storage = null)
|
||||
{
|
||||
if ($storage) {
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Exception;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
|
||||
/**
|
||||
* Interface DataInterface
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
interface DataInterface
|
||||
{
|
||||
/**
|
||||
@@ -34,35 +40,44 @@ interface DataInterface
|
||||
|
||||
/**
|
||||
* Return blueprints.
|
||||
*
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function blueprints();
|
||||
|
||||
/**
|
||||
* Validate by blueprints.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return $this
|
||||
* @throws Exception
|
||||
*/
|
||||
public function validate();
|
||||
|
||||
/**
|
||||
* Filter all items by using blueprints.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filter();
|
||||
|
||||
/**
|
||||
* Get extra items which haven't been defined in blueprints.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extra();
|
||||
|
||||
/**
|
||||
* Save data into the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Set or get the data storage.
|
||||
*
|
||||
* @param FileInterface $storage Optionally enter a new storage.
|
||||
* @param FileInterface|null $storage Optionally enter a new storage.
|
||||
* @return FileInterface
|
||||
*/
|
||||
public function file(FileInterface $storage = null);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Data
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RuntimeException;
|
||||
|
||||
class ValidationException extends \RuntimeException
|
||||
/**
|
||||
* Class ValidationException
|
||||
* @package Grav\Common\Data
|
||||
*/
|
||||
class ValidationException extends RuntimeException
|
||||
{
|
||||
/** @var array */
|
||||
protected $messages = [];
|
||||
|
||||
public function setMessages(array $messages = []) {
|
||||
/**
|
||||
* @param array $messages
|
||||
* @return $this
|
||||
*/
|
||||
public function setMessages(array $messages = [])
|
||||
{
|
||||
$this->messages = $messages;
|
||||
|
||||
$language = Grav::instance()['language'];
|
||||
$this->message = $language->translate('FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
|
||||
$this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
|
||||
|
||||
foreach ($messages as $variable => &$list) {
|
||||
$list = array_unique($list);
|
||||
@@ -30,6 +42,9 @@ class ValidationException extends \RuntimeException
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMessages()
|
||||
{
|
||||
return $this->messages;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Errors
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,22 +11,23 @@ namespace Grav\Common\Errors;
|
||||
|
||||
use Whoops\Handler\Handler;
|
||||
|
||||
/**
|
||||
* Class BareHandler
|
||||
* @package Grav\Common\Errors
|
||||
*/
|
||||
class BareHandler extends Handler
|
||||
{
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$inspector = $this->getInspector();
|
||||
$code = $inspector->getException()->getCode();
|
||||
if ( ($code >= 400) && ($code < 600) )
|
||||
{
|
||||
$this->getRun()->sendHttpCode($code);
|
||||
if (($code >= 400) && ($code < 600)) {
|
||||
$this->getRun()->sendHttpCode($code);
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Errors
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Grav;
|
||||
use Whoops;
|
||||
use Whoops\Handler\JsonResponseHandler;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run;
|
||||
use Whoops\Util\Misc;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* Class Errors
|
||||
* @package Grav\Common\Errors
|
||||
*/
|
||||
class Errors
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function resetHandlers()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$config = $grav['config']->get('system.errors');
|
||||
$jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/json';
|
||||
$jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json';
|
||||
|
||||
// Setup Whoops-based error handler
|
||||
$system = new SystemFacade;
|
||||
$whoops = new \Whoops\Run($system);
|
||||
$whoops = new Run($system);
|
||||
|
||||
$verbosity = 1;
|
||||
|
||||
@@ -35,42 +48,33 @@ class Errors
|
||||
|
||||
switch ($verbosity) {
|
||||
case 1:
|
||||
$error_page = new Whoops\Handler\PrettyPageHandler;
|
||||
$error_page = new PrettyPageHandler();
|
||||
$error_page->setPageTitle('Crikey! There was an error...');
|
||||
$error_page->addResourcePath(GRAV_ROOT . '/system/assets');
|
||||
$error_page->addCustomCss('whoops.css');
|
||||
$whoops->pushHandler($error_page);
|
||||
$whoops->prependHandler($error_page);
|
||||
break;
|
||||
case -1:
|
||||
$whoops->pushHandler(new BareHandler);
|
||||
$whoops->prependHandler(new BareHandler);
|
||||
break;
|
||||
default:
|
||||
$whoops->pushHandler(new SimplePageHandler);
|
||||
$whoops->prependHandler(new SimplePageHandler);
|
||||
break;
|
||||
}
|
||||
|
||||
if (method_exists('Whoops\Util\Misc', 'isAjaxRequest')) { //Whoops 2.0
|
||||
if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
|
||||
$whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
|
||||
}
|
||||
} elseif (function_exists('Whoops\isAjaxRequest')) { //Whoops 2.0.0-alpha
|
||||
if (Whoops\isAjaxRequest() || $jsonRequest) {
|
||||
$whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
|
||||
}
|
||||
} else { //Whoops 1.x
|
||||
$json_page = new Whoops\Handler\JsonResponseHandler;
|
||||
$json_page->onlyForAjaxRequests(true);
|
||||
if ($jsonRequest || Misc::isAjaxRequest()) {
|
||||
$whoops->prependHandler(new JsonResponseHandler());
|
||||
}
|
||||
|
||||
if (isset($config['log']) && $config['log']) {
|
||||
$logger = $grav['log'];
|
||||
$whoops->pushHandler(function($exception, $inspector, $run) use ($logger) {
|
||||
$whoops->pushHandler(function ($exception, $inspector, $run) use ($logger) {
|
||||
try {
|
||||
$logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
echo $e;
|
||||
}
|
||||
}, 'log');
|
||||
});
|
||||
}
|
||||
|
||||
$whoops->register();
|
||||
|
||||
@@ -1,54 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Errors
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use ErrorException;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Util\Misc;
|
||||
use Whoops\Util\TemplateHelper;
|
||||
|
||||
/**
|
||||
* Class SimplePageHandler
|
||||
* @package Grav\Common\Errors
|
||||
*/
|
||||
class SimplePageHandler extends Handler
|
||||
{
|
||||
private $searchPaths = array();
|
||||
private $resourceCache = array();
|
||||
/** @var array */
|
||||
private $searchPaths = [];
|
||||
/** @var array */
|
||||
private $resourceCache = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Add the default, local resource search path:
|
||||
$this->searchPaths[] = __DIR__ . "/Resources";
|
||||
$this->searchPaths[] = __DIR__ . '/Resources';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$inspector = $this->getInspector();
|
||||
|
||||
$helper = new TemplateHelper();
|
||||
$templateFile = $this->getResource("layout.html.php");
|
||||
$cssFile = $this->getResource("error.css");
|
||||
$templateFile = $this->getResource('layout.html.php');
|
||||
$cssFile = $this->getResource('error.css');
|
||||
|
||||
$code = $inspector->getException()->getCode();
|
||||
if ( ($code >= 400) && ($code < 600) )
|
||||
{
|
||||
$this->getRun()->sendHttpCode($code);
|
||||
if (($code >= 400) && ($code < 600)) {
|
||||
$this->getRun()->sendHttpCode($code);
|
||||
}
|
||||
$message = $inspector->getException()->getMessage();
|
||||
|
||||
if ($inspector->getException() instanceof \ErrorException) {
|
||||
if ($inspector->getException() instanceof ErrorException) {
|
||||
$code = Misc::translateErrorCode($code);
|
||||
}
|
||||
|
||||
$vars = array(
|
||||
"stylesheet" => file_get_contents($cssFile),
|
||||
"code" => $code,
|
||||
"message" => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING),
|
||||
'stylesheet' => file_get_contents($cssFile),
|
||||
'code' => $code,
|
||||
'message' => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING),
|
||||
);
|
||||
|
||||
$helper->setVariables($vars);
|
||||
@@ -58,10 +67,9 @@ class SimplePageHandler extends Handler
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $resource
|
||||
*
|
||||
* @param string $resource
|
||||
* @return string
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function getResource($resource)
|
||||
{
|
||||
@@ -74,7 +82,7 @@ class SimplePageHandler extends Handler
|
||||
// Search through available search paths, until we find the
|
||||
// resource we're after:
|
||||
foreach ($this->searchPaths as $path) {
|
||||
$fullPath = $path . "/$resource";
|
||||
$fullPath = "{$path}/{$resource}";
|
||||
|
||||
if (is_file($fullPath)) {
|
||||
// Cache the result:
|
||||
@@ -84,15 +92,19 @@ class SimplePageHandler extends Handler
|
||||
}
|
||||
|
||||
// If we got this far, nothing was found.
|
||||
throw new \RuntimeException(
|
||||
throw new RuntimeException(
|
||||
"Could not find resource '{$resource}' in any resource paths (searched: " . implode(', ', $this->searchPaths). ')'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return void
|
||||
*/
|
||||
public function addResourcePath($path)
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
throw new \InvalidArgumentException(
|
||||
throw new InvalidArgumentException(
|
||||
"'{$path}' is not a valid directory"
|
||||
);
|
||||
}
|
||||
@@ -100,6 +112,9 @@ class SimplePageHandler extends Handler
|
||||
array_unshift($this->searchPaths, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResourcePaths()
|
||||
{
|
||||
return $this->searchPaths;
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Errors
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
/**
|
||||
* Class SystemFacade
|
||||
* @package Grav\Common\Errors
|
||||
*/
|
||||
class SystemFacade extends \Whoops\Util\SystemFacade
|
||||
{
|
||||
/** @var callable */
|
||||
protected $whoopsShutdownHandler;
|
||||
|
||||
/**
|
||||
* @param callable $function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerShutdownFunction(callable $function)
|
||||
@@ -25,6 +30,8 @@ class SystemFacade extends \Whoops\Util\SystemFacade
|
||||
|
||||
/**
|
||||
* Special case to deal with Fatal errors and the like.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleShutdown()
|
||||
{
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.File
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\File;
|
||||
|
||||
use Exception;
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use function function_exists;
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
* Trait CompiledFile
|
||||
* @package Grav\Common\File
|
||||
*/
|
||||
trait CompiledFile
|
||||
{
|
||||
/**
|
||||
* Get/set parsed file contents.
|
||||
*
|
||||
* @param mixed $var
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function content($var = null)
|
||||
{
|
||||
@@ -27,9 +37,12 @@ trait CompiledFile
|
||||
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
|
||||
|
||||
$modified = $this->modified();
|
||||
|
||||
if (!$modified) {
|
||||
return $this->decode($this->raw());
|
||||
try {
|
||||
return $this->decode($this->raw());
|
||||
} catch (Throwable $e) {
|
||||
// If the compiled file is broken, we can safely ignore the error and continue.
|
||||
}
|
||||
}
|
||||
|
||||
$class = get_class($this);
|
||||
@@ -37,8 +50,7 @@ trait CompiledFile
|
||||
$cache = $file->exists() ? $file->content() : null;
|
||||
|
||||
// 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['modified'] !== $modified
|
||||
|| $cache['filename'] !== $this->filename
|
||||
@@ -46,7 +58,7 @@ trait CompiledFile
|
||||
// Attempt to lock the file for writing.
|
||||
try {
|
||||
$file->lock(false);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
// Another process has locked the file; we will check this in a bit.
|
||||
}
|
||||
|
||||
@@ -75,9 +87,8 @@ trait CompiledFile
|
||||
|
||||
$this->content = $cache['data'];
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
|
||||
}
|
||||
|
||||
return parent::content($var);
|
||||
@@ -85,6 +96,8 @@ trait CompiledFile
|
||||
|
||||
/**
|
||||
* Serialize file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.File
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,10 @@ namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
|
||||
/**
|
||||
* Class CompiledJsonFile
|
||||
* @package Grav\Common\File
|
||||
*/
|
||||
class CompiledJsonFile extends JsonFile
|
||||
{
|
||||
use CompiledFile;
|
||||
@@ -19,10 +24,10 @@ class CompiledJsonFile extends JsonFile
|
||||
*
|
||||
* @param string $var
|
||||
* @param bool $assoc
|
||||
* @return array mixed
|
||||
* @return array
|
||||
*/
|
||||
protected function decode($var, $assoc = true)
|
||||
{
|
||||
return (array) json_decode($var, $assoc);
|
||||
return (array)json_decode($var, $assoc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.File
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,10 @@ namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\MarkdownFile;
|
||||
|
||||
/**
|
||||
* Class CompiledMarkdownFile
|
||||
* @package Grav\Common\File
|
||||
*/
|
||||
class CompiledMarkdownFile extends MarkdownFile
|
||||
{
|
||||
use CompiledFile;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.File
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,10 @@ namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
|
||||
/**
|
||||
* Class CompiledYamlFile
|
||||
* @package Grav\Common\File
|
||||
*/
|
||||
class CompiledYamlFile extends YamlFile
|
||||
{
|
||||
use CompiledFile;
|
||||
|
||||
108
system/src/Grav/Common/Filesystem/Archiver.php
Normal file
108
system/src/Grav/Common/Filesystem/Archiver.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Utils;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use function function_exists;
|
||||
|
||||
/**
|
||||
* Class Archiver
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
abstract class Archiver
|
||||
{
|
||||
/** @var array */
|
||||
protected $options = [
|
||||
'exclude_files' => ['.DS_Store'],
|
||||
'exclude_paths' => []
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
protected $archive_file;
|
||||
|
||||
/**
|
||||
* @param string $compression
|
||||
* @return ZipArchiver
|
||||
*/
|
||||
public static function create($compression)
|
||||
{
|
||||
if ($compression === 'zip') {
|
||||
return new ZipArchiver();
|
||||
}
|
||||
|
||||
return new ZipArchiver();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $archive_file
|
||||
* @return $this
|
||||
*/
|
||||
public function setArchive($archive_file)
|
||||
{
|
||||
$this->archive_file = $archive_file;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return $this
|
||||
*/
|
||||
public function setOptions($options)
|
||||
{
|
||||
// Set infinite PHP execution time if possible.
|
||||
if (Utils::functionExists('set_time_limit')) {
|
||||
@set_time_limit(0);
|
||||
}
|
||||
|
||||
$this->options = $options + $this->options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function compress($folder, callable $status = null);
|
||||
|
||||
/**
|
||||
* @param string $destination
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function extract($destination, callable $status = null);
|
||||
|
||||
/**
|
||||
* @param array $folders
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function addEmptyFolders($folders, callable $status = null);
|
||||
|
||||
/**
|
||||
* @param string $rootPath
|
||||
* @return RecursiveIteratorIterator
|
||||
*/
|
||||
protected function getArchiveFiles($rootPath)
|
||||
{
|
||||
$exclude_paths = $this->options['exclude_paths'];
|
||||
$exclude_files = $this->options['exclude_files'];
|
||||
$dirItr = new RecursiveDirectoryIterator($rootPath, RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS);
|
||||
$filterItr = new RecursiveDirectoryFilterIterator($dirItr, $rootPath, $exclude_paths, $exclude_files);
|
||||
$files = new RecursiveIteratorIterator($filterItr, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.FileSystem
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Grav;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function is_callable;
|
||||
|
||||
/**
|
||||
* Class Folder
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
abstract class Folder
|
||||
{
|
||||
/**
|
||||
@@ -21,20 +36,23 @@ abstract class Folder
|
||||
*/
|
||||
public static function lastModifiedFolder($path)
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$last_modified = 0;
|
||||
|
||||
/** @var UniformResourceLocator $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);
|
||||
$directory = new RecursiveDirectoryIterator($path, $flags);
|
||||
}
|
||||
$filter = new RecursiveFolderFilterIterator($directory);
|
||||
$iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $dir) {
|
||||
$dir_modified = $dir->getMTime();
|
||||
if ($dir_modified > $last_modified) {
|
||||
@@ -48,34 +66,37 @@ abstract class Folder
|
||||
/**
|
||||
* Recursively find the last modified time under given path by file.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $path
|
||||
* @param string $extensions which files to search for specifically
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function lastModifiedFile($path, $extensions = 'md|yaml')
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$last_modified = 0;
|
||||
|
||||
/** @var UniformResourceLocator $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);
|
||||
$directory = new RecursiveDirectoryIterator($path, $flags);
|
||||
}
|
||||
$recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
|
||||
$recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
|
||||
$iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
/** @var RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $filepath => $file) {
|
||||
try {
|
||||
$file_modified = $file->getMTime();
|
||||
if ($file_modified > $last_modified) {
|
||||
$last_modified = $file_modified;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -86,26 +107,29 @@ abstract class Folder
|
||||
/**
|
||||
* Recursively md5 hash all files in a path
|
||||
*
|
||||
* @param $path
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public static function hashAllFiles($path)
|
||||
{
|
||||
$flags = \RecursiveDirectoryIterator::SKIP_DOTS;
|
||||
$files = [];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($path)) {
|
||||
$directory = $locator->getRecursiveIterator($path, $flags);
|
||||
} else {
|
||||
$directory = new \RecursiveDirectoryIterator($path, $flags);
|
||||
}
|
||||
if (file_exists($path)) {
|
||||
$flags = RecursiveDirectoryIterator::SKIP_DOTS;
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($path)) {
|
||||
$directory = $locator->getRecursiveIterator($path, $flags);
|
||||
} else {
|
||||
$directory = new RecursiveDirectoryIterator($path, $flags);
|
||||
}
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$files[] = $file->getPathname() . '?'. $file->getMTime();
|
||||
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$files[] = $file->getPathname() . '?'. $file->getMTime();
|
||||
}
|
||||
}
|
||||
|
||||
return md5(serialize($files));
|
||||
@@ -114,9 +138,8 @@ abstract class Folder
|
||||
/**
|
||||
* Get relative path between target and base path. If path isn't relative, return full path.
|
||||
*
|
||||
* @param string $path
|
||||
* @param mixed|string $base
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $base
|
||||
* @return string
|
||||
*/
|
||||
public static function getRelativePath($path, $base = GRAV_ROOT)
|
||||
@@ -141,6 +164,7 @@ abstract class Folder
|
||||
*/
|
||||
public static function getRelativePathDotDot($path, $base)
|
||||
{
|
||||
// Normalize paths.
|
||||
$base = preg_replace('![\\\/]+!', '/', $base);
|
||||
$path = preg_replace('![\\\/]+!', '/', $path);
|
||||
|
||||
@@ -148,8 +172,8 @@ abstract class Folder
|
||||
return '';
|
||||
}
|
||||
|
||||
$baseParts = explode('/', isset($base[0]) && '/' === $base[0] ? substr($base, 1) : $base);
|
||||
$pathParts = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path);
|
||||
$baseParts = explode('/', ltrim($base, '/'));
|
||||
$pathParts = explode('/', ltrim($path, '/'));
|
||||
|
||||
array_pop($baseParts);
|
||||
$lastPart = array_pop($pathParts);
|
||||
@@ -164,7 +188,7 @@ abstract class Folder
|
||||
$path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
|
||||
|
||||
return '' === $path
|
||||
|| '/' === $path[0]
|
||||
|| strpos($path, '/') === 0
|
||||
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
|
||||
? "./$path" : $path;
|
||||
}
|
||||
@@ -190,50 +214,53 @@ abstract class Folder
|
||||
* @param string $path
|
||||
* @param array $params
|
||||
* @return array
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function all($path, array $params = [])
|
||||
{
|
||||
if ($path === false) {
|
||||
throw new \RuntimeException("Path doesn't exist.");
|
||||
if (!$path) {
|
||||
throw new RuntimeException("Path doesn't exist.");
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
|
||||
$pattern = isset($params['pattern']) ? $params['pattern'] : null;
|
||||
$filters = isset($params['filters']) ? $params['filters'] : null;
|
||||
$recursive = isset($params['recursive']) ? $params['recursive'] : true;
|
||||
$levels = isset($params['levels']) ? $params['levels'] : -1;
|
||||
$pattern = $params['pattern'] ?? null;
|
||||
$filters = $params['filters'] ?? null;
|
||||
$recursive = $params['recursive'] ?? true;
|
||||
$levels = $params['levels'] ?? -1;
|
||||
$key = isset($params['key']) ? 'get' . $params['key'] : null;
|
||||
$value = isset($params['value']) ? 'get' . $params['value'] : ($recursive ? 'getSubPathname' : 'getFilename');
|
||||
$folders = isset($params['folders']) ? $params['folders'] : true;
|
||||
$files = isset($params['files']) ? $params['files'] : true;
|
||||
$value = 'get' . ($params['value'] ?? ($recursive ? 'SubPathname' : 'Filename'));
|
||||
$folders = $params['folders'] ?? true;
|
||||
$files = $params['files'] ?? true;
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($recursive) {
|
||||
$flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
|
||||
+ \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
|
||||
$flags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
|
||||
+ FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
|
||||
if ($locator->isStream($path)) {
|
||||
$directory = $locator->getRecursiveIterator($path, $flags);
|
||||
} 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);
|
||||
$iterator->setMaxDepth(max($levels, -1));
|
||||
} else {
|
||||
if ($locator->isStream($path)) {
|
||||
$iterator = $locator->getIterator($path);
|
||||
} else {
|
||||
$iterator = new \FilesystemIterator($path);
|
||||
$iterator = new FilesystemIterator($path);
|
||||
}
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
/** @var RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $file) {
|
||||
// Ignore hidden files.
|
||||
if ($file->getFilename()[0] === '.') {
|
||||
if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (!$folders && $file->isDir()) {
|
||||
@@ -255,7 +282,7 @@ abstract class Folder
|
||||
if (isset($filters['value'])) {
|
||||
$filter = $filters['value'];
|
||||
if (is_callable($filter)) {
|
||||
$filePath = call_user_func($filter, $file);
|
||||
$filePath = $filter($file);
|
||||
} else {
|
||||
$filePath = preg_replace($filter, '', $filePath);
|
||||
}
|
||||
@@ -277,8 +304,9 @@ abstract class Folder
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $target
|
||||
* @param string $ignore Ignore files matching pattern (regular expression).
|
||||
* @throws \RuntimeException
|
||||
* @param string|null $ignore Ignore files matching pattern (regular expression).
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function copy($source, $target, $ignore = null)
|
||||
{
|
||||
@@ -286,7 +314,7 @@ abstract class Folder
|
||||
$target = rtrim($target, '\\/');
|
||||
|
||||
if (!is_dir($source)) {
|
||||
throw new \RuntimeException('Cannot copy non-existing folder.');
|
||||
throw new RuntimeException('Cannot copy non-existing folder.');
|
||||
}
|
||||
|
||||
// Make sure that path to the target exists before copying.
|
||||
@@ -316,7 +344,7 @@ abstract class Folder
|
||||
|
||||
if (!$success) {
|
||||
$error = error_get_last();
|
||||
throw new \RuntimeException($error['message']);
|
||||
throw new RuntimeException($error['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Make sure that the change will be detected when caching.
|
||||
@@ -328,13 +356,14 @@ abstract class Folder
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $target
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function move($source, $target)
|
||||
{
|
||||
if (!file_exists($source) || !is_dir($source)) {
|
||||
// Rename fails if source folder does not exist.
|
||||
throw new \RuntimeException('Cannot move non-existing folder.');
|
||||
throw new RuntimeException('Cannot move non-existing folder.');
|
||||
}
|
||||
|
||||
// Don't do anything if the source is the same as the new target
|
||||
@@ -342,9 +371,13 @@ abstract class Folder
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos($target, $source) === 0) {
|
||||
throw new RuntimeException('Cannot move folder to itself');
|
||||
}
|
||||
|
||||
if (file_exists($target)) {
|
||||
// Rename fails if target folder exists.
|
||||
throw new \RuntimeException('Cannot move files to existing folder/file.');
|
||||
throw new RuntimeException('Cannot move files to existing folder/file.');
|
||||
}
|
||||
|
||||
// Make sure that path to the target exists before moving.
|
||||
@@ -354,11 +387,7 @@ abstract class Folder
|
||||
@rename($source, $target);
|
||||
|
||||
// Rename function can fail while still succeeding, so let's check if the folder exists.
|
||||
if (!file_exists($target) || !is_dir($target)) {
|
||||
// In some rare cases rename() creates file, not a folder. Get rid of it.
|
||||
if (file_exists($target)) {
|
||||
@unlink($target);
|
||||
}
|
||||
if (is_dir($source)) {
|
||||
// Rename doesn't support moving folders across filesystems. Use copy instead.
|
||||
self::copy($source, $target);
|
||||
self::delete($source);
|
||||
@@ -376,7 +405,7 @@ abstract class Folder
|
||||
* @param string $target
|
||||
* @param bool $include_target
|
||||
* @return bool
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function delete($target, $include_target = true)
|
||||
{
|
||||
@@ -388,7 +417,7 @@ abstract class Folder
|
||||
|
||||
if (!$success) {
|
||||
$error = error_get_last();
|
||||
throw new \RuntimeException($error['message']);
|
||||
throw new RuntimeException($error['message']);
|
||||
}
|
||||
|
||||
// Make sure that the change will be detected when caching.
|
||||
@@ -403,7 +432,8 @@ abstract class Folder
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function mkdir($folder)
|
||||
{
|
||||
@@ -412,30 +442,34 @@ abstract class Folder
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function create($folder)
|
||||
{
|
||||
if (is_dir($folder)) {
|
||||
// Silence error for open_basedir; should fail in mkdir instead.
|
||||
if (@is_dir($folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$success = @mkdir($folder, 0777, true);
|
||||
|
||||
if (!$success) {
|
||||
$error = error_get_last();
|
||||
throw new \RuntimeException($error['message']);
|
||||
// Take yet another look, make sure that the folder doesn't exist.
|
||||
clearstatcache(true, $folder);
|
||||
if (!@is_dir($folder)) {
|
||||
throw new RuntimeException(sprintf('Unable to create directory: %s', $folder));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive copy of one directory to another
|
||||
*
|
||||
* @param $src
|
||||
* @param $dest
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dest
|
||||
* @return bool
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function rcopy($src, $dest)
|
||||
{
|
||||
@@ -448,12 +482,11 @@ abstract class Folder
|
||||
|
||||
// If the destination directory does not exist create it
|
||||
if (!is_dir($dest)) {
|
||||
static::mkdir($dest);
|
||||
static::create($dest);
|
||||
}
|
||||
|
||||
// Open the source directory to read in files
|
||||
$i = new \DirectoryIterator($src);
|
||||
/** @var \DirectoryIterator $f */
|
||||
$i = new DirectoryIterator($src);
|
||||
foreach ($i as $f) {
|
||||
if ($f->isFile()) {
|
||||
copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
|
||||
@@ -466,6 +499,22 @@ abstract class Folder
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a directory contain children
|
||||
*
|
||||
* @param string $directory
|
||||
* @return int|false
|
||||
*/
|
||||
public static function countChildren($directory)
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return false;
|
||||
}
|
||||
$directories = glob($directory . '/*', GLOB_ONLYDIR);
|
||||
|
||||
return count($directories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @param bool $include_target
|
||||
@@ -475,7 +524,7 @@ abstract class Folder
|
||||
protected static function doDelete($folder, $include_target = true)
|
||||
{
|
||||
// Special case for symbolic links.
|
||||
if (is_link($folder)) {
|
||||
if ($include_target && is_link($folder)) {
|
||||
return @unlink($folder);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use RecursiveFilterIterator;
|
||||
use RecursiveIterator;
|
||||
use SplFileInfo;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Class RecursiveDirectoryFilterIterator
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
class RecursiveDirectoryFilterIterator extends RecursiveFilterIterator
|
||||
{
|
||||
/** @var string */
|
||||
protected static $root;
|
||||
/** @var array */
|
||||
protected static $ignore_folders;
|
||||
/** @var array */
|
||||
protected static $ignore_files;
|
||||
|
||||
/**
|
||||
* Create a RecursiveFilterIterator from a RecursiveIterator
|
||||
*
|
||||
* @param RecursiveIterator $iterator
|
||||
* @param string $root
|
||||
* @param array $ignore_folders
|
||||
* @param array $ignore_files
|
||||
*/
|
||||
public function __construct(RecursiveIterator $iterator, $root, $ignore_folders, $ignore_files)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
|
||||
$this::$root = $root;
|
||||
$this::$ignore_folders = $ignore_folders;
|
||||
$this::$ignore_files = $ignore_files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current element of the iterator is acceptable
|
||||
*
|
||||
* @return bool true if the current element is acceptable, otherwise false.
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
/** @var SplFileInfo $file */
|
||||
$file = $this->current();
|
||||
$filename = $file->getFilename();
|
||||
$relative_filename = str_replace($this::$root . '/', '', $file->getPathname());
|
||||
|
||||
if ($file->isDir()) {
|
||||
if (in_array($relative_filename, $this::$ignore_folders, true)) {
|
||||
return false;
|
||||
}
|
||||
if (!in_array($filename, $this::$ignore_files, true)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($file->isFile() && !in_array($filename, $this::$ignore_files, true)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RecursiveDirectoryFilterIterator|RecursiveFilterIterator
|
||||
*/
|
||||
public function getChildren()
|
||||
{
|
||||
/** @var RecursiveDirectoryFilterIterator $iterator */
|
||||
$iterator = $this->getInnerIterator();
|
||||
|
||||
return new self($iterator->getChildren(), $this::$root, $this::$ignore_folders, $this::$ignore_files);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.FileSystem
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RecursiveIterator;
|
||||
use SplFileInfo;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Class RecursiveFolderFilterIterator
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
protected static $folder_ignores;
|
||||
/** @var array */
|
||||
protected static $ignore_folders;
|
||||
|
||||
/**
|
||||
* Create a RecursiveFilterIterator from a RecursiveIterator
|
||||
*
|
||||
* @param \RecursiveIterator $iterator
|
||||
* @param RecursiveIterator $iterator
|
||||
* @param array $ignore_folders
|
||||
*/
|
||||
public function __construct(\RecursiveIterator $iterator)
|
||||
public function __construct(RecursiveIterator $iterator, $ignore_folders = [])
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
if (empty($this::$folder_ignores)) {
|
||||
$this::$folder_ignores = Grav::instance()['config']->get('system.pages.ignore_folders');
|
||||
|
||||
if (empty($ignore_folders)) {
|
||||
$ignore_folders = Grav::instance()['config']->get('system.pages.ignore_folders');
|
||||
}
|
||||
|
||||
$this::$ignore_folders = $ignore_folders;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,12 +47,9 @@ class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
/** @var $current \SplFileInfo */
|
||||
/** @var SplFileInfo $current */
|
||||
$current = $this->current();
|
||||
|
||||
if ($current->isDir() && !in_array($current->getFilename(), $this::$folder_ignores, true)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return $current->isDir() && !in_array($current->getFilename(), $this::$ignore_folders, true);
|
||||
}
|
||||
}
|
||||
|
||||
136
system/src/Grav/Common/Filesystem/ZipArchiver.php
Normal file
136
system/src/Grav/Common/Filesystem/ZipArchiver.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
use function extension_loaded;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class ZipArchiver
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
class ZipArchiver extends Archiver
|
||||
{
|
||||
/**
|
||||
* @param string $destination
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
public function extract($destination, callable $status = null)
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
$archive = $zip->open($this->archive_file);
|
||||
|
||||
if ($archive === true) {
|
||||
Folder::create($destination);
|
||||
|
||||
if (!$zip->extractTo($destination)) {
|
||||
throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
throw new RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $source
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
public function compress($source, callable $status = null)
|
||||
{
|
||||
if (!extension_loaded('zip')) {
|
||||
throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
|
||||
}
|
||||
|
||||
if (!file_exists($source)) {
|
||||
throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if (!$zip->open($this->archive_file, ZipArchive::CREATE)) {
|
||||
throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
|
||||
}
|
||||
|
||||
// Get real path for our folder
|
||||
$rootPath = realpath($source);
|
||||
|
||||
$files = $this->getArchiveFiles($rootPath);
|
||||
|
||||
$status && $status([
|
||||
'type' => 'count',
|
||||
'steps' => iterator_count($files),
|
||||
]);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filePath = $file->getPathname();
|
||||
$relativePath = ltrim(substr($filePath, strlen($rootPath)), '/');
|
||||
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($relativePath);
|
||||
} else {
|
||||
$zip->addFile($filePath, $relativePath);
|
||||
}
|
||||
|
||||
$status && $status([
|
||||
'type' => 'progress',
|
||||
]);
|
||||
}
|
||||
|
||||
$status && $status([
|
||||
'type' => 'message',
|
||||
'message' => 'Compressing...'
|
||||
]);
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $folders
|
||||
* @param callable|null $status
|
||||
* @return $this
|
||||
*/
|
||||
public function addEmptyFolders($folders, callable $status = null)
|
||||
{
|
||||
if (!extension_loaded('zip')) {
|
||||
throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if (!$zip->open($this->archive_file)) {
|
||||
throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...');
|
||||
}
|
||||
|
||||
$status && $status([
|
||||
'type' => 'message',
|
||||
'message' => 'Adding empty folders...'
|
||||
]);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$zip->addEmptyDir($folder);
|
||||
$status && $status([
|
||||
'type' => 'progress',
|
||||
]);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
28
system/src/Grav/Common/Flex/FlexCollection.php
Normal file
28
system/src/Grav/Common/Flex/FlexCollection.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex;
|
||||
|
||||
use Grav\Common\Flex\Traits\FlexCollectionTrait;
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
|
||||
/**
|
||||
* Class FlexCollection
|
||||
*
|
||||
* @package Grav\Common\Flex
|
||||
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
|
||||
* @extends \Grav\Framework\Flex\FlexCollection<T>
|
||||
*/
|
||||
abstract class FlexCollection extends \Grav\Framework\Flex\FlexCollection
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexCollectionTrait;
|
||||
}
|
||||
29
system/src/Grav/Common/Flex/FlexIndex.php
Normal file
29
system/src/Grav/Common/Flex/FlexIndex.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex;
|
||||
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
use Grav\Common\Flex\Traits\FlexIndexTrait;
|
||||
|
||||
/**
|
||||
* Class FlexIndex
|
||||
*
|
||||
* @package Grav\Common\Flex
|
||||
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
|
||||
* @template C of \Grav\Framework\Flex\Interfaces\FlexCollectionInterface
|
||||
* @extends \Grav\Framework\Flex\FlexIndex<T,C>
|
||||
*/
|
||||
abstract class FlexIndex extends \Grav\Framework\Flex\FlexIndex
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexIndexTrait;
|
||||
}
|
||||
73
system/src/Grav/Common/Flex/FlexObject.php
Normal file
73
system/src/Grav/Common/Flex/FlexObject.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex;
|
||||
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
use Grav\Common\Flex\Traits\FlexObjectTrait;
|
||||
use Grav\Framework\Flex\Traits\FlexMediaTrait;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class FlexObject
|
||||
*
|
||||
* @package Grav\Common\Flex
|
||||
*/
|
||||
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexObjectTrait;
|
||||
use FlexMediaTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::getFormValue()
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null)
|
||||
{
|
||||
$value = $this->getNestedProperty($name, null, $separator);
|
||||
|
||||
// Handle media order field.
|
||||
if (null === $value && $name === 'media_order') {
|
||||
return implode(',', $this->getMediaOrder());
|
||||
}
|
||||
|
||||
// Handle media fields.
|
||||
$settings = $this->getFieldSettings($name);
|
||||
if ($settings['media_field'] ?? false === true) {
|
||||
return $this->parseFileProperty($value, $settings);
|
||||
}
|
||||
|
||||
return $value ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::prepareStorage()
|
||||
*/
|
||||
public function prepareStorage(): array
|
||||
{
|
||||
// Remove extra content from media fields.
|
||||
$fields = $this->getMediaFields();
|
||||
foreach ($fields as $field) {
|
||||
$data = $this->getNestedProperty($field);
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $name => &$image) {
|
||||
unset($image['image_url'], $image['thumb_url']);
|
||||
}
|
||||
unset($image);
|
||||
$this->setNestedProperty($field, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::prepareStorage();
|
||||
}
|
||||
}
|
||||
51
system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
Normal file
51
system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Traits;
|
||||
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* Trait FlexCollectionTrait
|
||||
* @package Grav\Common\Flex\Traits
|
||||
*/
|
||||
trait FlexCollectionTrait
|
||||
{
|
||||
use FlexCommonTrait;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param object|null $event
|
||||
* @return $this
|
||||
*/
|
||||
public function triggerEvent(string $name, $event = null)
|
||||
{
|
||||
if (null === $event) {
|
||||
$event = new Event([
|
||||
'type' => 'flex',
|
||||
'directory' => $this->getFlexDirectory(),
|
||||
'collection' => $this
|
||||
]);
|
||||
}
|
||||
if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
|
||||
$name = 'onFlexCollection' . substr($name, 2);
|
||||
}
|
||||
|
||||
$container = $this->getContainer();
|
||||
if ($event instanceof Event) {
|
||||
$container->fireEvent($name, $event);
|
||||
} else {
|
||||
$container->dispatchEvent($event);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
54
system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
Normal file
54
system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Traits;
|
||||
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/**
|
||||
* Trait FlexCommonTrait
|
||||
* @package Grav\Common\Flex\Traits
|
||||
*/
|
||||
trait FlexCommonTrait
|
||||
{
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $container['twig'];
|
||||
|
||||
try {
|
||||
return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
|
||||
} catch (LoaderError $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function getTemplatePaths(string $layout): array;
|
||||
abstract protected function getContainer(): Grav;
|
||||
}
|
||||
74
system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
Normal file
74
system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Flex\Flex;
|
||||
|
||||
/**
|
||||
* Implements Grav specific logic
|
||||
*/
|
||||
trait FlexGravTrait
|
||||
{
|
||||
/**
|
||||
* @return Grav
|
||||
*/
|
||||
protected function getContainer(): Grav
|
||||
{
|
||||
return Grav::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Flex
|
||||
*/
|
||||
protected function getFlexContainer(): Flex
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
|
||||
/** @var Flex $flex */
|
||||
$flex = $container['flex'];
|
||||
|
||||
return $flex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserInterface|null
|
||||
*/
|
||||
protected function getActiveUser(): ?UserInterface
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
|
||||
/** @var UserInterface|null $user */
|
||||
$user = $container['user'] ?? null;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAdminSite(): bool
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
|
||||
return isset($container['admin']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizeScope(): string
|
||||
{
|
||||
return $this->isAdminSite() ? 'admin' : 'site';
|
||||
}
|
||||
}
|
||||
20
system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
Normal file
20
system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Traits;
|
||||
|
||||
/**
|
||||
* Trait FlexIndexTrait
|
||||
* @package Grav\Common\Flex\Traits
|
||||
*/
|
||||
trait FlexIndexTrait
|
||||
{
|
||||
}
|
||||
62
system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php
Normal file
62
system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Traits;
|
||||
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* Trait FlexObjectTrait
|
||||
* @package Grav\Common\Flex\Traits
|
||||
*/
|
||||
trait FlexObjectTrait
|
||||
{
|
||||
use FlexCommonTrait;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param object|null $event
|
||||
* @return $this
|
||||
*/
|
||||
public function triggerEvent(string $name, $event = null)
|
||||
{
|
||||
$events = [
|
||||
'onRender' => 'onFlexObjectRender',
|
||||
'onBeforeSave' => 'onFlexObjectBeforeSave',
|
||||
'onAfterSave' => 'onFlexObjectAfterSave',
|
||||
'onBeforeDelete' => 'onFlexObjectBeforeDelete',
|
||||
'onAfterDelete' => 'onFlexObjectAfterDelete'
|
||||
];
|
||||
|
||||
if (null === $event) {
|
||||
$event = new Event([
|
||||
'type' => 'flex',
|
||||
'directory' => $this->getFlexDirectory(),
|
||||
'object' => $this
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($events['name'])) {
|
||||
$name = $events['name'];
|
||||
} elseif (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) {
|
||||
$name = 'onFlexObject' . substr($name, 2);
|
||||
}
|
||||
|
||||
$container = $this->getContainer();
|
||||
if ($event instanceof Event) {
|
||||
$container->fireEvent($name, $event);
|
||||
} else {
|
||||
$container->dispatchEvent($event);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Generic;
|
||||
|
||||
use Grav\Common\Flex\FlexCollection;
|
||||
|
||||
/**
|
||||
* Class GenericCollection
|
||||
* @package Grav\Common\Flex\Generic
|
||||
*
|
||||
* @extends FlexCollection<GenericObject>
|
||||
*/
|
||||
class GenericCollection extends FlexCollection
|
||||
{
|
||||
}
|
||||
24
system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
Normal file
24
system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Generic;
|
||||
|
||||
use Grav\Common\Flex\FlexIndex;
|
||||
|
||||
/**
|
||||
* Class GenericIndex
|
||||
* @package Grav\Common\Flex\Generic
|
||||
*
|
||||
* @extends FlexIndex<GenericObject,GenericCollection>
|
||||
*/
|
||||
class GenericIndex extends FlexIndex
|
||||
{
|
||||
}
|
||||
22
system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
Normal file
22
system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Generic;
|
||||
|
||||
use Grav\Common\Flex\FlexObject;
|
||||
|
||||
/**
|
||||
* Class GenericObject
|
||||
* @package Grav\Common\Flex\Generic
|
||||
*/
|
||||
class GenericObject extends FlexObject
|
||||
{
|
||||
}
|
||||
811
system/src/Grav/Common/Flex/Types/Pages/PageCollection.php
Normal file
811
system/src/Grav/Common/Flex/Types/Pages/PageCollection.php
Normal file
@@ -0,0 +1,811 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Flex\Traits\FlexCollectionTrait;
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Header;
|
||||
use Grav\Common\Page\Interfaces\PageCollectionInterface;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Pages\FlexPageCollection;
|
||||
use Collator;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use function array_search;
|
||||
use function count;
|
||||
use function extension_loaded;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class GravPageCollection
|
||||
* @package Grav\Plugin\FlexObjects\Types\GravPages
|
||||
*
|
||||
* @extends FlexPageCollection<PageObject>
|
||||
*
|
||||
* Incompatibilities with Grav\Common\Page\Collection:
|
||||
* $page = $collection->key() will not work at all
|
||||
* $clone = clone $collection does not clone objects inside the collection, does it matter?
|
||||
* $string = (string)$collection returns collection id instead of comma separated list
|
||||
* $collection->add() incompatible method signature
|
||||
* $collection->remove() incompatible method signature
|
||||
* $collection->filter() incompatible method signature (takes closure instead of callable)
|
||||
* $collection->prev() does not rewind the internal pointer
|
||||
* AND most methods are immutable; they do not update the current collection, but return updated one
|
||||
*
|
||||
* @method static shuffle()
|
||||
* @method static select(array $keys)
|
||||
* @method static unselect(array $keys)
|
||||
* @method static createFrom(array $elements, string $keyField = null)
|
||||
* @method PageIndex getIndex()
|
||||
*/
|
||||
class PageCollection extends FlexPageCollection implements PageCollectionInterface
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexCollectionTrait;
|
||||
|
||||
/** @var array|null */
|
||||
protected $_params;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
// Collection specific methods
|
||||
'getRoot' => false,
|
||||
'getParams' => false,
|
||||
'setParams' => false,
|
||||
'params' => false,
|
||||
'addPage' => false,
|
||||
'merge' => false,
|
||||
'intersect' => false,
|
||||
'prev' => false,
|
||||
'nth' => false,
|
||||
'random' => false,
|
||||
'append' => false,
|
||||
'batch' => false,
|
||||
'order' => false,
|
||||
|
||||
// Collection filtering
|
||||
'dateRange' => true,
|
||||
'visible' => true,
|
||||
'nonVisible' => true,
|
||||
'pages' => true,
|
||||
'modules' => true,
|
||||
'modular' => true,
|
||||
'nonModular' => true,
|
||||
'published' => true,
|
||||
'nonPublished' => true,
|
||||
'routable' => true,
|
||||
'nonRoutable' => true,
|
||||
'ofType' => true,
|
||||
'ofOneOfTheseTypes' => true,
|
||||
'ofOneOfTheseAccessLevels' => true,
|
||||
'withOrdered' => true,
|
||||
'withModules' => true,
|
||||
'withPages' => true,
|
||||
'withTranslation' => true,
|
||||
'filterBy' => true,
|
||||
|
||||
'toExtendedArray' => false,
|
||||
'getLevelListing' => false,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PageObject
|
||||
*/
|
||||
public function getRoot()
|
||||
{
|
||||
$index = $this->getIndex();
|
||||
|
||||
return $index->getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection params
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->_params ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parameters to the Collection
|
||||
*
|
||||
* @param array $params
|
||||
* @return $this
|
||||
*/
|
||||
public function setParams(array $params)
|
||||
{
|
||||
$this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection params
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function params(): array
|
||||
{
|
||||
return $this->getParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single page to a collection
|
||||
*
|
||||
* @param PageInterface $page
|
||||
* @return static
|
||||
*/
|
||||
public function addPage(PageInterface $page)
|
||||
{
|
||||
if (!$page instanceof FlexObjectInterface) {
|
||||
throw new InvalidArgumentException('$page is not a flex page.');
|
||||
}
|
||||
|
||||
// FIXME: support other keys.
|
||||
$this->set($page->getKey(), $page);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Merge another collection with the current collection
|
||||
*
|
||||
* @param PageCollectionInterface $collection
|
||||
* @return static
|
||||
*/
|
||||
public function merge(PageCollectionInterface $collection)
|
||||
{
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Intersect another collection with the current collection
|
||||
*
|
||||
* @param PageCollectionInterface $collection
|
||||
* @return static
|
||||
*/
|
||||
public function intersect(PageCollectionInterface $collection)
|
||||
{
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return previous item.
|
||||
*
|
||||
* @return PageInterface|false
|
||||
* @phpstan-return PageObject|false
|
||||
*/
|
||||
public function prev()
|
||||
{
|
||||
// FIXME: this method does not rewind the internal pointer!
|
||||
$key = (string)$this->key();
|
||||
$prev = $this->prevSibling($key);
|
||||
|
||||
return $prev !== $this->current() ? $prev : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return nth item.
|
||||
* @param int $key
|
||||
* @return PageInterface|bool
|
||||
* @phpstan-return PageObject|false
|
||||
*/
|
||||
public function nth($key)
|
||||
{
|
||||
return $this->slice($key, 1)[0] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick one or more random entries.
|
||||
*
|
||||
* @param int $num Specifies how many entries should be picked.
|
||||
* @return static
|
||||
*/
|
||||
public function random($num = 1)
|
||||
{
|
||||
return $this->createFrom($this->shuffle()->slice(0, $num));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append new elements to the list.
|
||||
*
|
||||
* @param array $items Items to be appended. Existing keys will be overridden with the new values.
|
||||
* @return static
|
||||
*/
|
||||
public function append($items)
|
||||
{
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Split collection into array of smaller collections.
|
||||
*
|
||||
* @param int $size
|
||||
* @return static[]
|
||||
*/
|
||||
public function batch($size): array
|
||||
{
|
||||
$chunks = $this->chunk($size);
|
||||
|
||||
$list = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$list[] = $this->createFrom($chunk);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder collection.
|
||||
*
|
||||
* @param string $by
|
||||
* @param string $dir
|
||||
* @param array|null $manual
|
||||
* @param int|null $sort_flags
|
||||
* @return static
|
||||
*/
|
||||
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
|
||||
{
|
||||
if (!$this->count()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($by === 'random') {
|
||||
return $this->shuffle();
|
||||
}
|
||||
|
||||
$keys = $this->buildSort($by, $dir, $manual, $sort_flags);
|
||||
|
||||
return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $order_by
|
||||
* @param string $order_dir
|
||||
* @param array|null $manual
|
||||
* @param int|null $sort_flags
|
||||
* @return array
|
||||
*/
|
||||
protected function buildSort($order_by = 'default', $order_dir = 'asc', $manual = null, $sort_flags = null): array
|
||||
{
|
||||
// do this header query work only once
|
||||
$header_query = null;
|
||||
$header_default = null;
|
||||
if (strpos($order_by, 'header.') === 0) {
|
||||
$query = explode('|', str_replace('header.', '', $order_by), 2);
|
||||
$header_query = array_shift($query) ?? '';
|
||||
$header_default = array_shift($query);
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($this as $key => $child) {
|
||||
switch ($order_by) {
|
||||
case 'title':
|
||||
$list[$key] = $child->title();
|
||||
break;
|
||||
case 'date':
|
||||
$list[$key] = $child->date();
|
||||
$sort_flags = SORT_REGULAR;
|
||||
break;
|
||||
case 'modified':
|
||||
$list[$key] = $child->modified();
|
||||
$sort_flags = SORT_REGULAR;
|
||||
break;
|
||||
case 'publish_date':
|
||||
$list[$key] = $child->publishDate();
|
||||
$sort_flags = SORT_REGULAR;
|
||||
break;
|
||||
case 'unpublish_date':
|
||||
$list[$key] = $child->unpublishDate();
|
||||
$sort_flags = SORT_REGULAR;
|
||||
break;
|
||||
case 'slug':
|
||||
$list[$key] = $child->slug();
|
||||
break;
|
||||
case 'basename':
|
||||
$list[$key] = basename($key);
|
||||
break;
|
||||
case 'folder':
|
||||
$list[$key] = $child->folder();
|
||||
break;
|
||||
case 'manual':
|
||||
case 'default':
|
||||
default:
|
||||
if (is_string($header_query)) {
|
||||
/** @var Header $child_header */
|
||||
$child_header = $child->header();
|
||||
$header_value = $child_header->get($header_query);
|
||||
if (is_array($header_value)) {
|
||||
$list[$key] = implode(',', $header_value);
|
||||
} elseif ($header_value) {
|
||||
$list[$key] = $header_value;
|
||||
} else {
|
||||
$list[$key] = $header_default ?: $key;
|
||||
}
|
||||
$sort_flags = $sort_flags ?: SORT_REGULAR;
|
||||
break;
|
||||
}
|
||||
$list[$key] = $key;
|
||||
$sort_flags = $sort_flags ?: SORT_REGULAR;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $sort_flags) {
|
||||
$sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
|
||||
}
|
||||
|
||||
// else just sort the list according to specified key
|
||||
if (extension_loaded('intl') && Grav::instance()['config']->get('system.intl_enabled')) {
|
||||
$locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set
|
||||
$col = Collator::create($locale);
|
||||
if ($col) {
|
||||
$col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
|
||||
if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
|
||||
$list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
|
||||
return sprintf('%032d.', $number[0]);
|
||||
}, $list);
|
||||
if (!is_array($list)) {
|
||||
throw new RuntimeException('Internal Error');
|
||||
}
|
||||
|
||||
$list_vals = array_values($list);
|
||||
if (is_numeric(array_shift($list_vals))) {
|
||||
$sort_flags = Collator::SORT_REGULAR;
|
||||
} else {
|
||||
$sort_flags = Collator::SORT_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
$col->asort($list, $sort_flags);
|
||||
} else {
|
||||
asort($list, $sort_flags);
|
||||
}
|
||||
} else {
|
||||
asort($list, $sort_flags);
|
||||
}
|
||||
|
||||
// Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
|
||||
if (is_array($manual) && !empty($manual)) {
|
||||
$i = count($manual);
|
||||
$new_list = [];
|
||||
foreach ($list as $key => $dummy) {
|
||||
$child = $this[$key];
|
||||
$order = array_search($child->slug, $manual, true);
|
||||
if ($order === false) {
|
||||
$order = $i++;
|
||||
}
|
||||
$new_list[$key] = (int)$order;
|
||||
}
|
||||
|
||||
$list = $new_list;
|
||||
|
||||
// Apply manual ordering to the list.
|
||||
asort($list, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
if ($order_dir !== 'asc') {
|
||||
$list = array_reverse($list);
|
||||
}
|
||||
|
||||
return array_keys($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimicks Pages class.
|
||||
*
|
||||
* @return $this
|
||||
* @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the items between a set of date ranges of either the page date field (default) or
|
||||
* an arbitrary datetime page field where start date and end date are optional
|
||||
* Dates must be passed in as text that strtotime() can process
|
||||
* http://php.net/manual/en/function.strtotime.php
|
||||
*
|
||||
* @param string|null $startDate
|
||||
* @param string|null $endDate
|
||||
* @param string|null $field
|
||||
* @return static
|
||||
* @throws Exception
|
||||
*/
|
||||
public function dateRange($startDate = null, $endDate = null, $field = null)
|
||||
{
|
||||
$start = $startDate ? Utils::date2timestamp($startDate) : null;
|
||||
$end = $endDate ? Utils::date2timestamp($endDate) : null;
|
||||
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if (!$object) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
|
||||
|
||||
if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only visible pages
|
||||
*
|
||||
* @return static The collection with only visible pages
|
||||
*/
|
||||
public function visible()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && $object->visible()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-visible pages
|
||||
*
|
||||
* @return static The collection with only non-visible pages
|
||||
*/
|
||||
public function nonVisible()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && !$object->visible()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages
|
||||
*
|
||||
* @return static The collection with only pages
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
$entries = [];
|
||||
/**
|
||||
* @var int|string $key
|
||||
* @var PageInterface|null $object
|
||||
*/
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && !$object->isModule()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only modules
|
||||
*
|
||||
* @return static The collection with only modules
|
||||
*/
|
||||
public function modules()
|
||||
{
|
||||
$entries = [];
|
||||
/**
|
||||
* @var int|string $key
|
||||
* @var PageInterface|null $object
|
||||
*/
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && $object->isModule()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of modules()
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function modular()
|
||||
{
|
||||
return $this->modules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of pages()
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function nonModular()
|
||||
{
|
||||
return $this->pages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only published pages
|
||||
*
|
||||
* @return static The collection with only published pages
|
||||
*/
|
||||
public function published()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && $object->published()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-published pages
|
||||
*
|
||||
* @return static The collection with only non-published pages
|
||||
*/
|
||||
public function nonPublished()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && !$object->published()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only routable pages
|
||||
*
|
||||
* @return static The collection with only routable pages
|
||||
*/
|
||||
public function routable()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && $object->routable()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-routable pages
|
||||
*
|
||||
* @return static The collection with only non-routable pages
|
||||
*/
|
||||
public function nonRoutable()
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && !$object->routable()) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of the specified type
|
||||
*
|
||||
* @param string $type
|
||||
* @return static The collection
|
||||
*/
|
||||
public function ofType($type)
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && $object->template() === $type) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of one of the specified types
|
||||
*
|
||||
* @param string[] $types
|
||||
* @return static The collection
|
||||
*/
|
||||
public function ofOneOfTheseTypes($types)
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && in_array($object->template(), $types, true)) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of one of the specified access levels
|
||||
*
|
||||
* @param array $accessLevels
|
||||
* @return static The collection
|
||||
*/
|
||||
public function ofOneOfTheseAccessLevels($accessLevels)
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object && isset($object->header()->access)) {
|
||||
if (is_array($object->header()->access)) {
|
||||
//Multiple values for access
|
||||
$valid = false;
|
||||
|
||||
foreach ($object->header()->access as $index => $accessLevel) {
|
||||
if (is_array($accessLevel)) {
|
||||
foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
|
||||
if (in_array($innerAccessLevel, $accessLevels)) {
|
||||
$valid = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (in_array($index, $accessLevels)) {
|
||||
$valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($valid) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
} else {
|
||||
//Single value for access
|
||||
if (in_array($object->header()->access, $accessLevels)) {
|
||||
$entries[$key] = $object;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
*/
|
||||
public function withOrdered(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isOrdered', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
*/
|
||||
public function withModules(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isModule', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
*/
|
||||
public function withPages(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isPage', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return static
|
||||
*/
|
||||
public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback])));
|
||||
|
||||
return $bool ? $this->select($list) : $this->unselect($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return PageIndex
|
||||
*/
|
||||
public function withTranslated(string $languageCode = null, bool $fallback = null)
|
||||
{
|
||||
return $this->getIndex()->withTranslated($languageCode, $fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter pages by given filters.
|
||||
*
|
||||
* - search: string
|
||||
* - page_type: string|string[]
|
||||
* - modular: bool
|
||||
* - visible: bool
|
||||
* - routable: bool
|
||||
* - published: bool
|
||||
* - page: bool
|
||||
* - translated: bool
|
||||
*
|
||||
* @param array $filters
|
||||
* @param bool $recursive
|
||||
* @return static
|
||||
*/
|
||||
public function filterBy(array $filters, bool $recursive = false)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('filterBy', [$filters, $recursive])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extended version of this Collection with each page keyed by route
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function toExtendedArray(): array
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
if ($object) {
|
||||
$entries[$object->route()] = $object->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getLevelListing(array $options): array
|
||||
{
|
||||
/** @var PageIndex $index */
|
||||
$index = $this->getIndex();
|
||||
|
||||
return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : [];
|
||||
}
|
||||
}
|
||||
1156
system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
Normal file
1156
system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
Normal file
File diff suppressed because it is too large
Load Diff
696
system/src/Grav/Common/Flex/Types/Pages/PageObject.php
Normal file
696
system/src/Grav/Common/Flex/Types/Pages/PageObject.php
Normal file
@@ -0,0 +1,696 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages;
|
||||
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
use Grav\Common\Flex\Traits\FlexObjectTrait;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Flex\Types\Pages\Traits\PageContentTrait;
|
||||
use Grav\Common\Flex\Types\Pages\Traits\PageLegacyTrait;
|
||||
use Grav\Common\Flex\Types\Pages\Traits\PageRoutableTrait;
|
||||
use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use Grav\Framework\Flex\FlexObject;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Pages\FlexPageObject;
|
||||
use Grav\Framework\Route\Route;
|
||||
use Grav\Framework\Route\RouteFactory;
|
||||
use Grav\Plugin\Admin\Admin;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function func_get_args;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class GravPageObject
|
||||
* @package Grav\Plugin\FlexObjects\Types\GravPages
|
||||
*
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string $route
|
||||
* @property string $folder
|
||||
* @property int|false $order
|
||||
* @property string $template
|
||||
* @property string $language
|
||||
*/
|
||||
class PageObject extends FlexPageObject
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexObjectTrait;
|
||||
use PageContentTrait;
|
||||
use PageLegacyTrait;
|
||||
use PageRoutableTrait;
|
||||
use PageTranslateTrait;
|
||||
|
||||
/** @var string Language code, eg: 'en' */
|
||||
protected $language;
|
||||
/** @var string File format, eg. 'md' */
|
||||
protected $format;
|
||||
|
||||
/** @var bool */
|
||||
private $_initialized = false;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'path' => true,
|
||||
'full_order' => true,
|
||||
'filterBy' => true,
|
||||
'translated' => false,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(): void
|
||||
{
|
||||
if (!$this->_initialized) {
|
||||
Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
|
||||
$this->_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function translated(): bool
|
||||
{
|
||||
return $this->translatedLanguages(true) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $query
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getRoute($query = []): ?Route
|
||||
{
|
||||
$route = $this->route();
|
||||
if (null === $route) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$route = RouteFactory::createFromString($route);
|
||||
if ($lang = $route->getLanguage()) {
|
||||
$grav = Grav::instance();
|
||||
if (!$grav['config']->get('system.languages.include_default_lang')) {
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
if ($lang === $language->getDefault()) {
|
||||
$route = $route->withLanguage('');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($query)) {
|
||||
foreach ($query as $key => $value) {
|
||||
$route = $route->withQueryParam($key, $value);
|
||||
}
|
||||
} else {
|
||||
$route = $route->withAddedPath($query);
|
||||
}
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc PageInterface
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null)
|
||||
{
|
||||
$test = new stdClass();
|
||||
|
||||
$value = $this->pageContentValue($name, $test);
|
||||
if ($value !== $test) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
switch ($name) {
|
||||
case 'name':
|
||||
// TODO: this should not be template!
|
||||
return $this->getProperty('template');
|
||||
case 'route':
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/');
|
||||
return $key !== '/' ? $key : null;
|
||||
case 'full_route':
|
||||
return $this->hasKey() ? '/' . $this->getKey() : '';
|
||||
case 'full_order':
|
||||
return $this->full_order();
|
||||
case 'lang':
|
||||
return $this->getLanguage() ?? '';
|
||||
case 'translations':
|
||||
return $this->getLanguages();
|
||||
}
|
||||
|
||||
return parent::getFormValue($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::getCacheKey()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
$cacheKey = parent::getCacheKey();
|
||||
if ($cacheKey) {
|
||||
/** @var Language $language */
|
||||
$language = Grav::instance()['language'];
|
||||
$cacheKey .= '_' . $language->getActive();
|
||||
}
|
||||
|
||||
return $cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
protected function onBeforeSave(array $variables)
|
||||
{
|
||||
$reorder = $variables[0] ?? true;
|
||||
|
||||
$meta = $this->getMetaData();
|
||||
if (($meta['copy'] ?? false) === true) {
|
||||
$this->folder = $this->getKey();
|
||||
}
|
||||
|
||||
// Figure out storage path to the new route.
|
||||
$parentKey = $this->getProperty('parent_key');
|
||||
if ($parentKey !== '') {
|
||||
$parentRoute = $this->getProperty('route');
|
||||
|
||||
// Root page cannot be moved.
|
||||
if ($this->root()) {
|
||||
throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute));
|
||||
}
|
||||
|
||||
// Make sure page isn't being moved under itself.
|
||||
$key = $this->getStorageKey();
|
||||
|
||||
/** @var PageObject|null $parent */
|
||||
$parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null;
|
||||
if (!$parent) {
|
||||
// Page cannot be moved to non-existing location.
|
||||
throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute));
|
||||
}
|
||||
|
||||
// TODO: make sure that the page doesn't exist yet if moved/copied.
|
||||
}
|
||||
|
||||
if ($reorder === true && !$this->root()) {
|
||||
$reorder = $this->_reorder;
|
||||
}
|
||||
|
||||
// Force automatic reorder if item is supposed to be added to the last.
|
||||
if (!is_array($reorder) && (int)$this->order() >= 999999) {
|
||||
$reorder = [];
|
||||
}
|
||||
|
||||
// Reorder siblings.
|
||||
$siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
|
||||
|
||||
$data = $this->prepareStorage();
|
||||
unset($data['header']);
|
||||
|
||||
foreach ($siblings as $sibling) {
|
||||
$data = $sibling->prepareStorage();
|
||||
unset($data['header']);
|
||||
}
|
||||
|
||||
return ['reorder' => $reorder, 'siblings' => $siblings];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
protected function onSave(array $variables): array
|
||||
{
|
||||
/** @var PageCollection $siblings */
|
||||
$siblings = $variables['siblings'];
|
||||
foreach ($siblings as $sibling) {
|
||||
$sibling->save(false);
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $variables
|
||||
*/
|
||||
protected function onAfterSave(array $variables): void
|
||||
{
|
||||
$this->getFlexDirectory()->reloadIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $reorder
|
||||
* @return FlexObject|FlexObjectInterface
|
||||
*/
|
||||
public function save($reorder = true)
|
||||
{
|
||||
$variables = $this->onBeforeSave(func_get_args());
|
||||
|
||||
// Backwards compatibility with older plugins.
|
||||
$fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
|
||||
$grav = $this->getContainer();
|
||||
if ($fireEvents) {
|
||||
$self = $this;
|
||||
$grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
|
||||
if ($self !== $this) {
|
||||
throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.');
|
||||
}
|
||||
}
|
||||
|
||||
/** @var static $instance */
|
||||
$instance = parent::save();
|
||||
$variables = $this->onSave($variables);
|
||||
|
||||
$this->onAfterSave($variables);
|
||||
|
||||
// Backwards compatibility with older plugins.
|
||||
if ($fireEvents) {
|
||||
$grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
|
||||
}
|
||||
|
||||
// Reset original after save events have all been called.
|
||||
$this->_original = null;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PageObject
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$result = parent::delete();
|
||||
|
||||
// Backwards compatibility with older plugins.
|
||||
$fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
|
||||
if ($fireEvents) {
|
||||
$this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this]));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare move page to new location. Moves also everything that's under the current page.
|
||||
*
|
||||
* You need to call $this->save() in order to perform the move.
|
||||
*
|
||||
* @param PageInterface $parent New parent page.
|
||||
* @return $this
|
||||
*/
|
||||
public function move(PageInterface $parent)
|
||||
{
|
||||
if (!$parent instanceof FlexObjectInterface) {
|
||||
throw new RuntimeException('Failed: Parent is not Flex Object');
|
||||
}
|
||||
|
||||
$this->_reorder = [];
|
||||
$this->setProperty('parent_key', $parent->getStorageKey());
|
||||
$this->storeOriginal();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
|
||||
{
|
||||
// Special case: creating a new page means checking parent for its permissions.
|
||||
if ($action === 'create' && !$this->exists()) {
|
||||
$parent = $this->parent();
|
||||
if ($parent && method_exists($parent, 'isAuthorized')) {
|
||||
return $parent->isAuthorized($action, $scope, $user);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ordering
|
||||
* @return PageCollection|null
|
||||
*/
|
||||
protected function reorderSiblings(array $ordering)
|
||||
{
|
||||
$storageKey = $this->getMasterKey();
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
|
||||
$newParentKey = $this->getProperty('parent_key');
|
||||
$isMoved = $oldParentKey !== $newParentKey;
|
||||
$order = !$isMoved ? $this->order() : false;
|
||||
if ($order !== false) {
|
||||
$order = (int)$order;
|
||||
}
|
||||
|
||||
$parent = $this->parent();
|
||||
if (!$parent) {
|
||||
throw new RuntimeException('Cannot reorder a page which has no parent');
|
||||
}
|
||||
|
||||
/** @var PageCollection $siblings */
|
||||
$siblings = $parent->children();
|
||||
$siblings = $siblings->getCollection()->withOrdered();
|
||||
|
||||
// Handle special case where ordering isn't given.
|
||||
if ($ordering === []) {
|
||||
if ($order >= 999999) {
|
||||
// Set ordering to point to be the last item.
|
||||
$order = 0;
|
||||
foreach ($siblings as $sibling) {
|
||||
$order = max($order, (int)$sibling->order());
|
||||
}
|
||||
$this->order($order + 1);
|
||||
}
|
||||
|
||||
// Do not change sibling ordering.
|
||||
return null;
|
||||
}
|
||||
|
||||
$siblings = $siblings->orderBy(['order' => 'ASC']);
|
||||
|
||||
if ($storageKey !== null) {
|
||||
if ($order !== false) {
|
||||
// Add current page back to the list if it's ordered.
|
||||
$siblings->set($storageKey, $this);
|
||||
} else {
|
||||
// Remove old copy of the current page from the siblings list.
|
||||
$siblings->remove($storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing siblings into the end of the list, keeping the previous ordering between them.
|
||||
foreach ($siblings as $sibling) {
|
||||
$basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
|
||||
if (!in_array($basename, $ordering, true)) {
|
||||
$ordering[] = $basename;
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder.
|
||||
$ordering = array_flip(array_values($ordering));
|
||||
$count = count($ordering);
|
||||
foreach ($siblings as $sibling) {
|
||||
$basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
|
||||
$newOrder = $ordering[$basename] ?? null;
|
||||
$newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
|
||||
$sibling->order($newOrder);
|
||||
}
|
||||
|
||||
$siblings = $siblings->orderBy(['order' => 'ASC']);
|
||||
$siblings->removeElement($this);
|
||||
|
||||
// If menu item was moved, just make it to be the last in order.
|
||||
if ($isMoved && $this->order() !== false) {
|
||||
$parentKey = $this->getProperty('parent_key');
|
||||
if ($parentKey === '') {
|
||||
$newParent = $this->getFlexDirectory()->getIndex()->getRoot();
|
||||
} else {
|
||||
$newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
|
||||
if (!$newParent instanceof PageInterface) {
|
||||
throw new RuntimeException("New parent page '{$parentKey}' not found.");
|
||||
}
|
||||
}
|
||||
/** @var PageCollection $newSiblings */
|
||||
$newSiblings = $newParent->children();
|
||||
$newSiblings = $newSiblings->getCollection()->withOrdered();
|
||||
$order = 0;
|
||||
foreach ($newSiblings as $sibling) {
|
||||
$order = max($order, (int)$sibling->order());
|
||||
}
|
||||
$this->order($order + 1);
|
||||
}
|
||||
|
||||
return $siblings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function full_order(): string
|
||||
{
|
||||
$route = $this->path() . '/' . $this->folder();
|
||||
|
||||
return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Blueprint
|
||||
*/
|
||||
protected function doGetBlueprint(string $name = ''): Blueprint
|
||||
{
|
||||
try {
|
||||
// Make sure that pages has been initialized.
|
||||
Pages::getTypes();
|
||||
|
||||
// TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here.
|
||||
if ($name === 'raw') {
|
||||
// Admin RAW mode.
|
||||
if ($this->isAdminSite()) {
|
||||
/** @var Admin $admin */
|
||||
$admin = Grav::instance()['admin'];
|
||||
|
||||
$template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw');
|
||||
|
||||
return $admin->blueprints("admin/pages/{$template}");
|
||||
}
|
||||
}
|
||||
|
||||
$template = $this->getProperty('template') . ($name ? '.' . $name : '');
|
||||
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
|
||||
} catch (RuntimeException $e) {
|
||||
$template = 'default' . ($name ? '.' . $name : '');
|
||||
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
|
||||
}
|
||||
|
||||
$isNew = $blueprint->get('initialized', false) === false;
|
||||
if ($isNew === true && $name === '') {
|
||||
// Support onBlueprintCreated event just like in Pages::blueprints($template)
|
||||
$blueprint->set('initialized', true);
|
||||
Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getLevelListing(array $options): array
|
||||
{
|
||||
$index = $this->getFlexDirectory()->getIndex();
|
||||
if (!is_callable([$index, 'getLevelListing'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Deal with relative paths.
|
||||
$initial = $options['initial'] ?? null;
|
||||
$var = $initial ? 'leaf_route' : 'route';
|
||||
$route = $options[$var] ?? '';
|
||||
if ($route !== '' && !str_starts_with($route, '/')) {
|
||||
$filesystem = Filesystem::getInstance();
|
||||
|
||||
$route = "/{$this->getKey()}/{$route}";
|
||||
$route = $filesystem->normalize($route);
|
||||
|
||||
$options[$var] = $route;
|
||||
}
|
||||
|
||||
[$status, $message, $response,] = $index->getLevelListing($options);
|
||||
|
||||
return [$status, $message, $response, $options[$var] ?? null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter page (true/false) by given filters.
|
||||
*
|
||||
* - search: string
|
||||
* - extension: string
|
||||
* - module: bool
|
||||
* - visible: bool
|
||||
* - routable: bool
|
||||
* - published: bool
|
||||
* - page: bool
|
||||
* - translated: bool
|
||||
*
|
||||
* @param array $filters
|
||||
* @param bool $recursive
|
||||
* @return bool
|
||||
*/
|
||||
public function filterBy(array $filters, bool $recursive = false): bool
|
||||
{
|
||||
foreach ($filters as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'search':
|
||||
$matches = $this->search((string)$value) > 0.0;
|
||||
break;
|
||||
case 'page_type':
|
||||
$types = $value ? explode(',', $value) : [];
|
||||
$matches = in_array($this->template(), $types, true);
|
||||
break;
|
||||
case 'extension':
|
||||
$matches = Utils::contains((string)$value, $this->extension());
|
||||
break;
|
||||
case 'routable':
|
||||
$matches = $this->isRoutable() === (bool)$value;
|
||||
break;
|
||||
case 'published':
|
||||
$matches = $this->isPublished() === (bool)$value;
|
||||
break;
|
||||
case 'visible':
|
||||
$matches = $this->isVisible() === (bool)$value;
|
||||
break;
|
||||
case 'module':
|
||||
$matches = $this->isModule() === (bool)$value;
|
||||
break;
|
||||
case 'page':
|
||||
$matches = $this->isPage() === (bool)$value;
|
||||
break;
|
||||
case 'folder':
|
||||
$matches = $this->isPage() === !$value;
|
||||
break;
|
||||
case 'translated':
|
||||
$matches = $this->hasTranslation() === (bool)$value;
|
||||
break;
|
||||
default:
|
||||
$matches = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If current filter does not match, we still may have match as a parent.
|
||||
if ($matches === false) {
|
||||
return $recursive && $this->children()->getIndex()->filterBy($filters, true)->count() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::exists()
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->root ?: parent::exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
$list = parent::__debugInfo();
|
||||
|
||||
return $list + [
|
||||
'_content_meta:private' => $this->getContentMeta(),
|
||||
'_content:private' => $this->getRawContent()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param bool $extended
|
||||
*/
|
||||
protected function filterElements(array &$elements, bool $extended = false): void
|
||||
{
|
||||
// Change parent page if needed.
|
||||
if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) {
|
||||
$elements['template'] = $elements['name'];
|
||||
|
||||
// Figure out storage path to the new route.
|
||||
$parentKey = trim($elements['route'] ?? '', '/');
|
||||
if ($parentKey !== '') {
|
||||
/** @var PageObject|null $parent */
|
||||
$parent = $this->getFlexDirectory()->getObject($parentKey);
|
||||
$parentKey = $parent ? $parent->getStorageKey() : $parentKey;
|
||||
}
|
||||
|
||||
$elements['parent_key'] = $parentKey;
|
||||
}
|
||||
|
||||
// Deal with ordering=bool and order=page1,page2,page3.
|
||||
if ($this->root()) {
|
||||
// Root page doesn't have ordering.
|
||||
unset($elements['ordering'], $elements['order']);
|
||||
} elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
|
||||
// Store ordering.
|
||||
$ordering = $elements['order'] ?? null;
|
||||
$this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
|
||||
|
||||
$order = false;
|
||||
if ((bool)($elements['ordering'] ?? false)) {
|
||||
$order = $this->order();
|
||||
if ($order === false) {
|
||||
$order = 999999;
|
||||
}
|
||||
}
|
||||
|
||||
$elements['order'] = $order;
|
||||
}
|
||||
|
||||
parent::filterElements($elements, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function prepareStorage(): array
|
||||
{
|
||||
$meta = $this->getMetaData();
|
||||
$oldLang = $meta['lang'] ?? '';
|
||||
$newLang = $this->getProperty('lang') ?? '';
|
||||
|
||||
// Always clone the page to the new language.
|
||||
if ($oldLang !== $newLang) {
|
||||
$meta['clone'] = true;
|
||||
}
|
||||
|
||||
// Make sure that certain elements are always sent to the storage layer.
|
||||
$elements = [
|
||||
'__META' => $meta,
|
||||
'storage_key' => $this->getStorageKey(),
|
||||
'parent_key' => $this->getProperty('parent_key'),
|
||||
'order' => $this->getProperty('order'),
|
||||
'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
|
||||
'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
|
||||
'lang' => $newLang
|
||||
] + parent::prepareStorage();
|
||||
|
||||
return $elements;
|
||||
}
|
||||
}
|
||||
700
system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
Normal file
700
system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
Normal file
@@ -0,0 +1,700 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages\Storage;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Flex\Types\Pages\PageIndex;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use Grav\Framework\Flex\Storage\FolderStorage;
|
||||
use RocketTheme\Toolbox\File\MarkdownFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
use function assert;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class GravPageStorage
|
||||
* @package Grav\Plugin\FlexObjects\Types\GravPages
|
||||
*/
|
||||
class PageStorage extends FolderStorage
|
||||
{
|
||||
/** @var bool */
|
||||
protected $ignore_hidden;
|
||||
/** @var array */
|
||||
protected $ignore_files;
|
||||
/** @var array */
|
||||
protected $ignore_folders;
|
||||
/** @var bool */
|
||||
protected $include_default_lang_file_extension;
|
||||
/** @var bool */
|
||||
protected $recurse;
|
||||
/** @var string */
|
||||
protected $base_path;
|
||||
|
||||
/** @var int */
|
||||
protected $flags;
|
||||
/** @var string */
|
||||
protected $regex;
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
protected function initOptions(array $options): void
|
||||
{
|
||||
parent::initOptions($options);
|
||||
|
||||
$this->flags = FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::CURRENT_AS_FILEINFO
|
||||
| FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
$config = $grav['config'];
|
||||
$this->ignore_hidden = (bool)$config->get('system.pages.ignore_hidden');
|
||||
$this->ignore_files = (array)$config->get('system.pages.ignore_files');
|
||||
$this->ignore_folders = (array)$config->get('system.pages.ignore_folders');
|
||||
$this->include_default_lang_file_extension = (bool)$config->get('system.languages.include_default_lang_file_extension', true);
|
||||
$this->recurse = (bool)($options['recurse'] ?? true);
|
||||
$this->regex = '/(\.([\w\d_-]+))?\.md$/D';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $variations
|
||||
* @return array
|
||||
*/
|
||||
public function parseKey(string $key, bool $variations = true): array
|
||||
{
|
||||
if (mb_strpos($key, '|') !== false) {
|
||||
[$key, $params] = explode('|', $key, 2);
|
||||
} else {
|
||||
$params = '';
|
||||
}
|
||||
$key = ltrim($key, '/');
|
||||
|
||||
$keys = parent::parseKey($key, false) + ['params' => $params];
|
||||
|
||||
if ($variations) {
|
||||
$keys += $this->parseParams($key, $params);
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function readFrontmatter(string $key): string
|
||||
{
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
try {
|
||||
if ($file instanceof MarkdownFile) {
|
||||
$frontmatter = $file->frontmatter();
|
||||
} else {
|
||||
$frontmatter = $file->raw();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
$frontmatter = 'ERROR: ' . $e->getMessage();
|
||||
} finally {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
|
||||
return $frontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function readRaw(string $key): string
|
||||
{
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
try {
|
||||
$raw = $file->raw();
|
||||
} catch (RuntimeException $e) {
|
||||
$raw = 'ERROR: ' . $e->getMessage();
|
||||
} finally {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param bool $includeParams
|
||||
* @return string
|
||||
*/
|
||||
public function buildStorageKey(array $keys, bool $includeParams = true): string
|
||||
{
|
||||
$key = $keys['key'] ?? null;
|
||||
if (null === $key) {
|
||||
$key = $keys['parent_key'] ?? '';
|
||||
if ($key !== '') {
|
||||
$key .= '/';
|
||||
}
|
||||
$order = $keys['order'] ?? null;
|
||||
$folder = $keys['folder'] ?? 'undefined';
|
||||
$key .= is_numeric($order) ? sprintf('%02d.%s', $order, $folder) : $folder;
|
||||
}
|
||||
|
||||
$params = $includeParams ? $this->buildStorageKeyParams($keys) : '';
|
||||
|
||||
return $params ? "{$key}|{$params}" : $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @return string
|
||||
*/
|
||||
public function buildStorageKeyParams(array $keys): string
|
||||
{
|
||||
$params = $keys['template'] ?? '';
|
||||
$language = $keys['lang'] ?? '';
|
||||
if ($language) {
|
||||
$params .= '.' . $language;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @return string
|
||||
*/
|
||||
public function buildFolder(array $keys): string
|
||||
{
|
||||
return $this->dataFolder . '/' . $this->buildStorageKey($keys, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @return string
|
||||
*/
|
||||
public function buildFilename(array $keys): string
|
||||
{
|
||||
$file = $this->buildStorageKeyParams($keys);
|
||||
|
||||
// Template is optional; if it is missing, we need to have to load the object metadata.
|
||||
if ($file && $file[0] === '.') {
|
||||
$meta = $this->getObjectMeta($this->buildStorageKey($keys, false));
|
||||
$file = ($meta['template'] ?? 'folder') . $file;
|
||||
}
|
||||
|
||||
return $file . $this->dataExt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @return string
|
||||
*/
|
||||
public function buildFilepath(array $keys): string
|
||||
{
|
||||
$folder = $this->buildFolder($keys);
|
||||
$filename = $this->buildFilename($keys);
|
||||
|
||||
return rtrim($folder, '/') !== $folder ? $folder . $filename : $folder . '/' . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @param bool $setDefaultLang
|
||||
* @return array
|
||||
*/
|
||||
public function extractKeysFromRow(array $row, bool $setDefaultLang = true): array
|
||||
{
|
||||
$meta = $row['__META'] ?? null;
|
||||
$storageKey = $row['storage_key'] ?? $meta['storage_key'] ?? '';
|
||||
$keyMeta = $storageKey !== '' ? $this->extractKeysFromStorageKey($storageKey) : null;
|
||||
$parentKey = $row['parent_key'] ?? $meta['parent_key'] ?? $keyMeta['parent_key'] ?? '';
|
||||
$order = $row['order'] ?? $meta['order'] ?? $keyMeta['order'] ?? null;
|
||||
$folder = $row['folder'] ?? $meta['folder'] ?? $keyMeta['folder'] ?? '';
|
||||
$template = $row['template'] ?? $meta['template'] ?? $keyMeta['template'] ?? '';
|
||||
$lang = $row['lang'] ?? $meta['lang'] ?? $keyMeta['lang'] ?? '';
|
||||
|
||||
// Handle default language, if it should be saved without language extension.
|
||||
if ($setDefaultLang && empty($meta['markdown'][$lang])) {
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$default = $language->getDefault();
|
||||
// Make sure that the default language file doesn't exist before overriding it.
|
||||
if (empty($meta['markdown'][$default])) {
|
||||
if ($this->include_default_lang_file_extension) {
|
||||
if ($lang === '') {
|
||||
$lang = $language->getDefault();
|
||||
}
|
||||
} elseif ($lang === $language->getDefault()) {
|
||||
$lang = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$keys = [
|
||||
'key' => null,
|
||||
'params' => null,
|
||||
'parent_key' => $parentKey,
|
||||
'order' => is_numeric($order) ? (int)$order : null,
|
||||
'folder' => $folder,
|
||||
'template' => $template,
|
||||
'lang' => $lang
|
||||
];
|
||||
|
||||
$keys['key'] = $this->buildStorageKey($keys, false);
|
||||
$keys['params'] = $this->buildStorageKeyParams($keys);
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public function extractKeysFromStorageKey(string $key): array
|
||||
{
|
||||
if (mb_strpos($key, '|') !== false) {
|
||||
[$key, $params] = explode('|', $key, 2);
|
||||
[$template, $language] = mb_strpos($params, '.') !== false ? explode('.', $params, 2) : [$params, ''];
|
||||
} else {
|
||||
$params = $template = $language = '';
|
||||
}
|
||||
$objectKey = basename($key);
|
||||
if (preg_match('|^(\d+)\.(.+)$|', $objectKey, $matches)) {
|
||||
[, $order, $folder] = $matches;
|
||||
} else {
|
||||
[$order, $folder] = ['', $objectKey];
|
||||
}
|
||||
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
|
||||
$parentKey = ltrim($filesystem->dirname('/' . $key), '/');
|
||||
|
||||
return [
|
||||
'key' => $key,
|
||||
'params' => $params,
|
||||
'parent_key' => $parentKey,
|
||||
'order' => is_numeric($order) ? (int)$order : null,
|
||||
'folder' => $folder,
|
||||
'template' => $template,
|
||||
'lang' => $language
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $params
|
||||
* @return array
|
||||
*/
|
||||
protected function parseParams(string $key, string $params): array
|
||||
{
|
||||
if (mb_strpos($params, '.') !== false) {
|
||||
[$template, $language] = explode('.', $params, 2);
|
||||
} else {
|
||||
$template = $params;
|
||||
$language = '';
|
||||
}
|
||||
|
||||
if ($template === '') {
|
||||
$meta = $this->getObjectMeta($key);
|
||||
$template = $meta['template'] ?? 'folder';
|
||||
}
|
||||
|
||||
return [
|
||||
'file' => $template . ($language ? '.' . $language : ''),
|
||||
'template' => $template,
|
||||
'lang' => $language
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the row for saving and returns the storage key for the record.
|
||||
*
|
||||
* @param array $row
|
||||
*/
|
||||
protected function prepareRow(array &$row): void
|
||||
{
|
||||
// Remove keys used in the filesystem.
|
||||
unset($row['parent_key'], $row['order'], $row['folder'], $row['template'], $row['lang']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
protected function loadRow(string $key): ?array
|
||||
{
|
||||
$data = parent::loadRow($key);
|
||||
|
||||
// Special case for root page.
|
||||
if ($key === '' && null !== $data) {
|
||||
$data['root'] = true;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page storage supports moving and copying the pages and their languages.
|
||||
*
|
||||
* $row['__META']['copy'] = true Use this if you want to copy the whole folder, otherwise it will be moved
|
||||
* $row['__META']['clone'] = true Use this if you want to clone the file, otherwise it will be renamed
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function saveRow(string $key, array $row): array
|
||||
{
|
||||
// Initialize all key-related variables.
|
||||
$newKeys = $this->extractKeysFromRow($row);
|
||||
$newKey = $this->buildStorageKey($newKeys);
|
||||
$newFolder = $this->buildFolder($newKeys);
|
||||
$newFilename = $this->buildFilename($newKeys);
|
||||
$newFilepath = rtrim($newFolder, '/') !== $newFolder ? $newFolder . $newFilename : $newFolder . '/' . $newFilename;
|
||||
|
||||
try {
|
||||
if ($key === '' && empty($row['root'])) {
|
||||
throw new RuntimeException('Page has no path');
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addMessage("Save page: {$newKey}", 'debug');
|
||||
|
||||
// Check if the row already exists.
|
||||
$oldKey = $row['__META']['storage_key'] ?? null;
|
||||
if (is_string($oldKey)) {
|
||||
// Initialize all old key-related variables.
|
||||
$oldKeys = $this->extractKeysFromRow(['__META' => $row['__META']], false);
|
||||
$oldFolder = $this->buildFolder($oldKeys);
|
||||
$oldFilename = $this->buildFilename($oldKeys);
|
||||
|
||||
// Check if folder has changed.
|
||||
if ($oldFolder !== $newFolder && file_exists($oldFolder)) {
|
||||
$isCopy = $row['__META']['copy'] ?? false;
|
||||
if ($isCopy) {
|
||||
if (strpos($newFolder, $oldFolder . '/') === 0) {
|
||||
throw new RuntimeException(sprintf('Page /%s cannot be copied to itself', $oldKey));
|
||||
}
|
||||
|
||||
$this->copyRow($oldKey, $newKey);
|
||||
$debugger->addMessage("Page copied: {$oldFolder} => {$newFolder}", 'debug');
|
||||
} else {
|
||||
if (strpos($newFolder, $oldFolder . '/') === 0) {
|
||||
throw new RuntimeException(sprintf('Page /%s cannot be moved to itself', $oldKey));
|
||||
}
|
||||
|
||||
$this->renameRow($oldKey, $newKey);
|
||||
$debugger->addMessage("Page moved: {$oldFolder} => {$newFolder}", 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if filename has changed.
|
||||
if ($oldFilename !== $newFilename) {
|
||||
// Get instance of the old file (we have already copied/moved it).
|
||||
$oldFilepath = "{$newFolder}/{$oldFilename}";
|
||||
$file = $this->getFile($oldFilepath);
|
||||
|
||||
// Rename the file if we aren't supposed to clone it.
|
||||
$isClone = $row['__META']['clone'] ?? false;
|
||||
if (!$isClone && $file->exists()) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$toPath = $locator->isStream($newFilepath) ? $locator->findResource($newFilepath, true, true) : GRAV_ROOT . "/{$newFilepath}";
|
||||
$success = $file->rename($toPath);
|
||||
if (!$success) {
|
||||
throw new RuntimeException("Changing page template failed: {$oldFilepath} => {$newFilepath}");
|
||||
}
|
||||
$debugger->addMessage("Page template changed: {$oldFilename} => {$newFilename}", 'debug');
|
||||
} else {
|
||||
$file = null;
|
||||
$debugger->addMessage("Page template created: {$newFilename}", 'debug');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the data to be saved.
|
||||
$this->prepareRow($row);
|
||||
unset($row['__META'], $row['__ERROR']);
|
||||
|
||||
if (!isset($file)) {
|
||||
$file = $this->getFile($newFilepath);
|
||||
}
|
||||
|
||||
// Compare existing file content to the new one and save the file only if content has been changed.
|
||||
$file->free();
|
||||
$oldRaw = $file->raw();
|
||||
$file->content($row);
|
||||
$newRaw = $file->raw();
|
||||
if ($oldRaw !== $newRaw) {
|
||||
$file->save($row);
|
||||
$debugger->addMessage("Page content saved: {$newFilepath}", 'debug');
|
||||
} else {
|
||||
$debugger->addMessage('Page content has not been changed, do not update the file', 'debug');
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
$name = isset($file) ? $file->filename() : $newKey;
|
||||
|
||||
throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $name, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
|
||||
if (isset($file)) {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
}
|
||||
|
||||
$row['__META'] = $this->getObjectMeta($newKey, true);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page folder should be deleted.
|
||||
*
|
||||
* Deleting page can be done either by deleting everything or just a single language.
|
||||
* If key contains the language, delete only it, unless it is the last language.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDeleteFolder(string $key): bool
|
||||
{
|
||||
// Return true if there's no language in the key.
|
||||
$keys = $this->extractKeysFromStorageKey($key);
|
||||
if (!$keys['lang']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the main key and reload meta.
|
||||
$key = $this->buildStorageKey($keys);
|
||||
$meta = $this->getObjectMeta($key, true);
|
||||
|
||||
// Return true if there aren't any markdown files left.
|
||||
return empty($meta['markdown'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key from the filesystem path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
if ($this->base_path) {
|
||||
$path = $this->base_path . '/' . $path;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
return $this->getIndexMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
protected function getObjectMeta(string $key, bool $reload = false): array
|
||||
{
|
||||
$keys = $this->extractKeysFromStorageKey($key);
|
||||
$key = $keys['key'];
|
||||
|
||||
if ($reload || !isset($this->meta[$key])) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if (mb_strpos($key, '@@') === false) {
|
||||
$path = $this->getStoragePath($key);
|
||||
if (is_string($path)) {
|
||||
$path = $locator->isStream($path) ? $locator->findResource($path) : GRAV_ROOT . "/{$path}";
|
||||
} else {
|
||||
$path = null;
|
||||
}
|
||||
} else {
|
||||
$path = null;
|
||||
}
|
||||
|
||||
$modified = 0;
|
||||
$markdown = [];
|
||||
$children = [];
|
||||
|
||||
if (is_string($path) && is_dir($path)) {
|
||||
$modified = filemtime($path);
|
||||
$iterator = new FilesystemIterator($path, $this->flags);
|
||||
|
||||
/** @var SplFileInfo $info */
|
||||
foreach ($iterator as $k => $info) {
|
||||
// Ignore all hidden files if set.
|
||||
if ($k === '' || ($this->ignore_hidden && $k[0] === '.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($info->isDir()) {
|
||||
// Ignore all folders in ignore list.
|
||||
if ($this->ignore_folders && in_array($k, $this->ignore_folders, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$children[$k] = false;
|
||||
} else {
|
||||
// Ignore all files in ignore list.
|
||||
if ($this->ignore_files && in_array($k, $this->ignore_files, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timestamp = $info->getMTime();
|
||||
|
||||
// Page is the one that matches to $page_extensions list with the lowest index number.
|
||||
if (preg_match($this->regex, $k, $matches)) {
|
||||
$mark = $matches[2] ?? '';
|
||||
$ext = $matches[1] ?? '';
|
||||
$ext .= $this->dataExt;
|
||||
$markdown[$mark][basename($k, $ext)] = $timestamp;
|
||||
}
|
||||
|
||||
$modified = max($modified, $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rawRoute = trim(preg_replace(PageIndex::PAGE_ROUTE_REGEX, '/', "/{$key}") ?? '', '/');
|
||||
$route = PageIndex::normalizeRoute($rawRoute);
|
||||
|
||||
ksort($markdown, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
ksort($children, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
$file = array_key_first($markdown[''] ?? (reset($markdown) ?: []));
|
||||
|
||||
$meta = [
|
||||
'key' => $route,
|
||||
'storage_key' => $key,
|
||||
'template' => $file,
|
||||
'storage_timestamp' => $modified,
|
||||
];
|
||||
if ($markdown) {
|
||||
$meta['markdown'] = $markdown;
|
||||
}
|
||||
if ($children) {
|
||||
$meta['children'] = $children;
|
||||
}
|
||||
$meta['checksum'] = md5(json_encode($meta) ?: '');
|
||||
|
||||
// Cache meta as copy.
|
||||
$this->meta[$key] = $meta;
|
||||
} else {
|
||||
$meta = $this->meta[$key];
|
||||
}
|
||||
|
||||
$params = $keys['params'];
|
||||
if ($params) {
|
||||
$language = $keys['lang'];
|
||||
$template = $keys['template'] ?: array_key_first($meta['markdown'][$language]) ?? $meta['template'];
|
||||
$meta['exists'] = ($template && !empty($meta['children'])) || isset($meta['markdown'][$language][$template]);
|
||||
$meta['storage_key'] .= '|' . $params;
|
||||
$meta['template'] = $template;
|
||||
$meta['lang'] = $language;
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getIndexMeta(): array
|
||||
{
|
||||
$queue = [''];
|
||||
$list = [];
|
||||
do {
|
||||
$current = array_pop($queue);
|
||||
if ($current === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
$meta = $this->getObjectMeta($current);
|
||||
$storage_key = $meta['storage_key'];
|
||||
|
||||
if (!empty($meta['children'])) {
|
||||
$prefix = $storage_key . ($storage_key !== '' ? '/' : '');
|
||||
|
||||
foreach ($meta['children'] as $child => $value) {
|
||||
$queue[] = $prefix . $child;
|
||||
}
|
||||
}
|
||||
|
||||
$list[$storage_key] = $meta;
|
||||
} while ($queue);
|
||||
|
||||
ksort($list, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
// Update parent timestamps.
|
||||
foreach (array_reverse($list) as $storage_key => $meta) {
|
||||
if ($storage_key !== '') {
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
|
||||
$storage_key = (string)$storage_key;
|
||||
$parentKey = $filesystem->dirname($storage_key);
|
||||
if ($parentKey === '.') {
|
||||
$parentKey = '';
|
||||
}
|
||||
|
||||
/** @phpstan-var array{'storage_key': string, 'storage_timestamp': int, 'children': array<string, mixed>} $parent */
|
||||
$parent = &$list[$parentKey];
|
||||
$basename = basename($storage_key);
|
||||
|
||||
if (isset($parent['children'][$basename])) {
|
||||
$timestamp = $meta['storage_timestamp'];
|
||||
$parent['children'][$basename] = $timestamp;
|
||||
if ($basename && $basename[0] === '_') {
|
||||
$parent['storage_timestamp'] = max($parent['storage_timestamp'], $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey(): string
|
||||
{
|
||||
throw new RuntimeException('Generating random key is disabled for pages');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages\Traits;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Implements PageContentInterface.
|
||||
*/
|
||||
trait PageContentTrait
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function id($var = null): string
|
||||
{
|
||||
$property = 'id';
|
||||
$value = null === $var ? $this->getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $this->language() . ($var ?? ($this->modified() . md5($this->filePath() ?? $this->getKey())));
|
||||
|
||||
$this->setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = $this->getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function date($var = null): int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'date',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false;
|
||||
|
||||
if (!$value) {
|
||||
// Get the specific translation updated date.
|
||||
$meta = $this->getMetaData();
|
||||
$language = $meta['lang'] ?? '';
|
||||
$template = $this->getProperty('template');
|
||||
$value = $meta['markdown'][$language][$template] ?? 0;
|
||||
}
|
||||
|
||||
return $value ?: $this->modified();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param bool $bool
|
||||
*/
|
||||
public function isPage(bool $bool = true): bool
|
||||
{
|
||||
$meta = $this->getMetaData();
|
||||
|
||||
return empty($meta['markdown']) !== $bool;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Collection;
|
||||
use Grav\Common\Page\Interfaces\PageCollectionInterface;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Implements PageLegacyInterface.
|
||||
*/
|
||||
trait PageLegacyTrait
|
||||
{
|
||||
/**
|
||||
* Returns children of this page.
|
||||
*
|
||||
* @return FlexIndexInterface|PageCollectionInterface|Collection
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::children();
|
||||
}
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
$path = $this->path() ?? '';
|
||||
|
||||
return $pages->children($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the first in an array of sub-pages.
|
||||
*
|
||||
* @return bool True if item is first.
|
||||
*/
|
||||
public function isFirst(): bool
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::isFirst();
|
||||
}
|
||||
|
||||
$path = $this->path();
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if (null !== $path && $collection instanceof PageCollectionInterface) {
|
||||
return $collection->isFirst($path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the last in an array of sub-pages.
|
||||
*
|
||||
* @return bool True if item is last
|
||||
*/
|
||||
public function isLast(): bool
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::isLast();
|
||||
}
|
||||
|
||||
$path = $this->path();
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if (null !== $path && $collection instanceof PageCollectionInterface) {
|
||||
return $collection->isLast($path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adjacent sibling based on a direction.
|
||||
*
|
||||
* @param int $direction either -1 or +1
|
||||
* @return PageInterface|false the sibling page
|
||||
*/
|
||||
public function adjacentSibling($direction = 1)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::adjacentSibling($direction);
|
||||
}
|
||||
|
||||
$path = $this->path();
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if (null !== $path && $collection instanceof PageCollectionInterface) {
|
||||
return $collection->adjacentSibling($path, $direction);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to return an ancestor page.
|
||||
*
|
||||
* @param string|null $lookup Name of the parent folder
|
||||
* @return PageInterface|null page you were looking for if it exists
|
||||
*/
|
||||
public function ancestor($lookup = null)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::ancestor($lookup);
|
||||
}
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
return $pages->ancestor($this->getProperty('parent_route'), $lookup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that contains shared logic for inherited() and inheritedField()
|
||||
*
|
||||
* @param string $field Name of the parent folder
|
||||
* @return array
|
||||
*/
|
||||
protected function getInheritedParams($field): array
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::getInheritedParams($field);
|
||||
}
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
$inherited = $pages->inherited($this->getProperty('parent_route'), $field);
|
||||
$inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : [];
|
||||
$currentParams = (array)$this->getFormValue('header.' . $field);
|
||||
if ($inheritedParams && is_array($inheritedParams)) {
|
||||
$currentParams = array_replace_recursive($inheritedParams, $currentParams);
|
||||
}
|
||||
|
||||
return [$inherited, $currentParams];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to return a page.
|
||||
*
|
||||
* @param string $url the url of the page
|
||||
* @param bool $all
|
||||
* @return PageInterface|null page you were looking for if it exists
|
||||
*/
|
||||
public function find($url, $all = false)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::find($url, $all);
|
||||
}
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
return $pages->find($url, $all);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of pages in the current context.
|
||||
*
|
||||
* @param string|array $params
|
||||
* @param bool $pagination
|
||||
* @return PageCollectionInterface|Collection
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function collection($params = 'content', $pagination = true)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::collection($params, $pagination);
|
||||
}
|
||||
|
||||
if (is_string($params)) {
|
||||
// Look into a page header field.
|
||||
$params = (array)$this->getFormValue('header.' . $params);
|
||||
} elseif (!is_array($params)) {
|
||||
throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
|
||||
}
|
||||
|
||||
$context = [
|
||||
'pagination' => $pagination,
|
||||
'self' => $this
|
||||
];
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
return $pages->getCollection($params, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $value
|
||||
* @param bool $only_published
|
||||
* @return PageCollectionInterface|Collection
|
||||
*/
|
||||
public function evaluate($value, $only_published = true)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::collection($value, $only_published);
|
||||
}
|
||||
|
||||
$params = [
|
||||
'items' => $value,
|
||||
'published' => $only_published
|
||||
];
|
||||
$context = [
|
||||
'event' => false,
|
||||
'pagination' => false,
|
||||
'url_taxonomy_filters' => false,
|
||||
'self' => $this
|
||||
];
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
return $pages->getCollection($params, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageCollectionInterface;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Implements PageRoutableInterface.
|
||||
*/
|
||||
trait PageRoutableTrait
|
||||
{
|
||||
/**
|
||||
* Gets and Sets the parent object for this page
|
||||
*
|
||||
* @param PageInterface|null $var the parent page object
|
||||
* @return PageInterface|null the parent page object if it exists.
|
||||
*/
|
||||
|
||||
public function parent(PageInterface $var = null)
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::parent();
|
||||
}
|
||||
|
||||
if (null !== $var) {
|
||||
throw new RuntimeException('Not Implemented');
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = Grav::instance()['pages'];
|
||||
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
|
||||
// FIXME: this does not work, needs to use $pages->get() with cached parent id!
|
||||
$key = $this->getKey();
|
||||
$parent_route = $filesystem->dirname('/' . $key);
|
||||
|
||||
return $parent_route !== '/' ? $pages->find($parent_route) : $pages->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @return int|null the index of the current page.
|
||||
*/
|
||||
public function currentPosition(): ?int
|
||||
{
|
||||
$path = $this->path();
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if (null !== $path && $collection instanceof PageCollectionInterface) {
|
||||
return $collection->currentPosition($path);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently active page requested via the URL.
|
||||
*
|
||||
* @return bool True if it is active
|
||||
*/
|
||||
public function active(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/';
|
||||
$routes = $grav['pages']->routes();
|
||||
|
||||
return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this URI's URL contains the URL of the active page.
|
||||
* Or in other words, is this page's URL in the current URL
|
||||
*
|
||||
* @return bool True if active child exists
|
||||
*/
|
||||
public function activeChild(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
/** @var Pages $pages */
|
||||
$pages = $grav['pages'];
|
||||
$uri_path = rtrim(urldecode($uri->path()), '/');
|
||||
$routes = $pages->routes();
|
||||
|
||||
if (isset($routes[$uri_path])) {
|
||||
$page = $pages->find($uri->route());
|
||||
/** @var PageInterface|null $child_page */
|
||||
$child_page = $page ? $page->parent() : null;
|
||||
while ($child_page && !$child_page->root()) {
|
||||
if ($this->path() === $child_page->path()) {
|
||||
return true;
|
||||
}
|
||||
$child_page = $child_page->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Pages\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Implements PageTranslateInterface
|
||||
*/
|
||||
trait PageTranslateTrait
|
||||
{
|
||||
/**
|
||||
* Return an array with the routes of other translated languages
|
||||
*
|
||||
* @param bool $onlyPublished only return published translations
|
||||
* @return array the page translated languages
|
||||
*/
|
||||
public function translatedLanguages($onlyPublished = false): array
|
||||
{
|
||||
if (Utils::isAdminPlugin()) {
|
||||
return parent::translatedLanguages();
|
||||
}
|
||||
|
||||
$translated = $this->getLanguageTemplates();
|
||||
if (!$translated) {
|
||||
return $translated;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$languages = $language->getLanguages();
|
||||
$languages[] = '';
|
||||
$defaultCode = $language->getDefault();
|
||||
|
||||
if (isset($translated[$defaultCode])) {
|
||||
unset($translated['']);
|
||||
}
|
||||
|
||||
foreach ($translated as $key => &$template) {
|
||||
$template .= $key !== '' ? ".{$key}.md" : '.md';
|
||||
}
|
||||
unset($template);
|
||||
|
||||
$translated = array_intersect_key($translated, array_flip($languages));
|
||||
|
||||
$folder = $this->getStorageFolder();
|
||||
if (!$folder) {
|
||||
return [];
|
||||
}
|
||||
$folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
|
||||
|
||||
$list = array_fill_keys($languages, null);
|
||||
foreach ($translated as $languageCode => $languageFile) {
|
||||
$languageExtension = $languageCode ? ".{$languageCode}.md" : '.md';
|
||||
$path = "{$folder}/{$languageFile}";
|
||||
|
||||
// FIXME: use flex, also rawRoute() does not fully work?
|
||||
$aPage = new Page();
|
||||
$aPage->init(new SplFileInfo($path), $languageExtension);
|
||||
if ($onlyPublished && !$aPage->published()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$header = $aPage->header();
|
||||
// @phpstan-ignore-next-line
|
||||
$routes = $header->routes ?? [];
|
||||
$route = $routes['default'] ?? $aPage->rawRoute();
|
||||
if (!$route) {
|
||||
$route = $aPage->route();
|
||||
}
|
||||
|
||||
$list[$languageCode ?: $defaultCode] = $route ?? '';
|
||||
}
|
||||
|
||||
$list = array_filter($list, static function ($var) {
|
||||
return null !== $var;
|
||||
});
|
||||
|
||||
// Hack to get the same result as with old pages.
|
||||
foreach ($list as &$path) {
|
||||
if ($path === '') {
|
||||
$path = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\UserGroups;
|
||||
|
||||
use Grav\Common\Flex\FlexCollection;
|
||||
|
||||
/**
|
||||
* Class UserGroupCollection
|
||||
* @package Grav\Common\Flex\Types\UserGroups
|
||||
*
|
||||
* @extends FlexCollection<UserGroupObject>
|
||||
*/
|
||||
class UserGroupCollection extends FlexCollection
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'authorize' => 'session',
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @return bool|null
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null): ?bool
|
||||
{
|
||||
$authorized = null;
|
||||
/** @var UserGroupObject $object */
|
||||
foreach ($this as $object) {
|
||||
$auth = $object->authorize($action, $scope);
|
||||
if ($auth === true) {
|
||||
$authorized = true;
|
||||
} elseif ($auth === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\UserGroups;
|
||||
|
||||
use Grav\Common\Flex\FlexIndex;
|
||||
|
||||
/**
|
||||
* Class GroupIndex
|
||||
* @package Grav\Common\User\FlexUser
|
||||
*
|
||||
* @extends FlexIndex<UserGroupObject,UserGroupCollection>
|
||||
*/
|
||||
class UserGroupIndex extends FlexIndex
|
||||
{
|
||||
}
|
||||
113
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
Normal file
113
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\UserGroups;
|
||||
|
||||
use Grav\Common\Flex\FlexObject;
|
||||
use Grav\Common\User\Access;
|
||||
use Grav\Common\User\Interfaces\UserGroupInterface;
|
||||
use function is_bool;
|
||||
|
||||
/**
|
||||
* Flex User Group
|
||||
*
|
||||
* @package Grav\Common\User
|
||||
*
|
||||
* @property string $groupname
|
||||
* @property Access $access
|
||||
*/
|
||||
class UserGroupObject extends FlexObject implements UserGroupInterface
|
||||
{
|
||||
/** @var Access */
|
||||
protected $_access;
|
||||
/** @var array|null */
|
||||
protected $access;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'authorize' => 'session',
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @return bool|null
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null): ?bool
|
||||
{
|
||||
if ($scope === 'test') {
|
||||
$scope = null;
|
||||
} elseif (!$this->getProperty('enabled', true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$access = $this->getAccess();
|
||||
|
||||
$authorized = $access->authorize($action, $scope);
|
||||
if (is_bool($authorized)) {
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
return $access->authorize('admin.super') ? true : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Access
|
||||
*/
|
||||
protected function getAccess(): Access
|
||||
{
|
||||
if (null === $this->_access) {
|
||||
$this->getProperty('access');
|
||||
}
|
||||
|
||||
return $this->_access;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetLoad_access($value): array
|
||||
{
|
||||
if (!$value instanceof Access) {
|
||||
$value = new Access($value);
|
||||
}
|
||||
|
||||
$this->_access = $value;
|
||||
|
||||
return $value->jsonSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetPrepare_access($value): array
|
||||
{
|
||||
return $this->offsetLoad_access($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $value
|
||||
* @return array|null
|
||||
*/
|
||||
protected function offsetSerialize_access(?array $value): ?array
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users\Storage;
|
||||
|
||||
use Grav\Framework\Flex\Storage\FileStorage;
|
||||
|
||||
/**
|
||||
* Class UserFileStorage
|
||||
* @package Grav\Common\Flex\Types\Users\Storage
|
||||
*/
|
||||
class UserFileStorage extends FileStorage
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): ?string
|
||||
{
|
||||
// There is no media support for file storage (fallback to common location).
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the row for saving and returns the storage key for the record.
|
||||
*
|
||||
* @param array $row
|
||||
*/
|
||||
protected function prepareRow(array &$row): void
|
||||
{
|
||||
parent::prepareRow($row);
|
||||
|
||||
$access = $row['access'] ?? [];
|
||||
unset($row['access']);
|
||||
if ($access) {
|
||||
$row['access'] = $access;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users\Storage;
|
||||
|
||||
use Grav\Framework\Flex\Storage\FolderStorage;
|
||||
|
||||
/**
|
||||
* Class UserFolderStorage
|
||||
* @package Grav\Common\Flex\Types\Users\Storage
|
||||
*/
|
||||
class UserFolderStorage extends FolderStorage
|
||||
{
|
||||
/**
|
||||
* Prepares the row for saving and returns the storage key for the record.
|
||||
*
|
||||
* @param array $row
|
||||
*/
|
||||
protected function prepareRow(array &$row): void
|
||||
{
|
||||
parent::prepareRow($row);
|
||||
|
||||
$access = $row['access'] ?? [];
|
||||
unset($row['access']);
|
||||
if ($access) {
|
||||
$row['access'] = $access;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users\Traits;
|
||||
|
||||
use Grav\Common\Page\Medium\ImageMedium;
|
||||
use Grav\Common\Page\Medium\StaticImageMedium;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Trait UserObjectLegacyTrait
|
||||
* @package Grav\Common\Flex\Types\Users\Traits
|
||||
*/
|
||||
trait UserObjectLegacyTrait
|
||||
{
|
||||
/**
|
||||
* Merge two configurations together.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
* @deprecated 1.6 Use `->update($data)` instead (same but with data validation & filtering, file upload support).
|
||||
*/
|
||||
public function merge(array $data)
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED);
|
||||
|
||||
$this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return media object for the User's avatar.
|
||||
*
|
||||
* @return ImageMedium|StaticImageMedium|null
|
||||
* @deprecated 1.6 Use ->getAvatarImage() method instead.
|
||||
*/
|
||||
public function getAvatarMedia()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getAvatarImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the User's avatar URL
|
||||
*
|
||||
* @return string
|
||||
* @deprecated 1.6 Use ->getAvatarUrl() method instead.
|
||||
*/
|
||||
public function avatarUrl()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getAvatarUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
* Ensures backwards compatibility
|
||||
*
|
||||
* @param string $action
|
||||
* @return bool
|
||||
* @deprecated 1.5 Use ->authorize() method instead.
|
||||
*/
|
||||
public function authorise($action)
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->authorize($action) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements Countable interface.
|
||||
*
|
||||
* @return int
|
||||
* @deprecated 1.6 Method makes no sense for user account.
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
|
||||
|
||||
return count($this->jsonSerialize());
|
||||
}
|
||||
}
|
||||
135
system/src/Grav/Common/Flex/Types/Users/UserCollection.php
Normal file
135
system/src/Grav/Common/Flex/Types/Users/UserCollection.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users;
|
||||
|
||||
use Grav\Common\Flex\FlexCollection;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class UserCollection
|
||||
* @package Grav\Common\Flex\Types\Users
|
||||
*
|
||||
* @extends FlexCollection<UserObject>
|
||||
*/
|
||||
class UserCollection extends FlexCollection implements UserCollectionInterface
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'authorize' => 'session',
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user account.
|
||||
*
|
||||
* Always creates user object. To check if user exists, use $this->exists().
|
||||
*
|
||||
* @param string $username
|
||||
* @return UserObject
|
||||
*/
|
||||
public function load($username): UserInterface
|
||||
{
|
||||
$username = (string)$username;
|
||||
|
||||
if ($username !== '') {
|
||||
$key = $this->filterUsername($username);
|
||||
$user = $this->get($key);
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
} else {
|
||||
$key = '';
|
||||
}
|
||||
|
||||
$directory = $this->getFlexDirectory();
|
||||
|
||||
/** @var UserObject $object */
|
||||
$object = $directory->createObject(
|
||||
[
|
||||
'username' => $username,
|
||||
'state' => 'enabled'
|
||||
],
|
||||
$key
|
||||
);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by username, email, etc
|
||||
*
|
||||
* @param string $query the query to search for
|
||||
* @param string|string[] $fields the fields to search
|
||||
* @return UserObject
|
||||
*/
|
||||
public function find($query, $fields = ['username', 'email']): UserInterface
|
||||
{
|
||||
if (is_string($query) && $query !== '') {
|
||||
foreach ((array)$fields as $field) {
|
||||
if ($field === 'key') {
|
||||
$user = $this->get($query);
|
||||
} elseif ($field === 'storage_key') {
|
||||
$user = $this->withKeyField('storage_key')->get($query);
|
||||
} elseif ($field === 'flex_key') {
|
||||
$user = $this->withKeyField('flex_key')->get($query);
|
||||
} elseif ($field === 'username') {
|
||||
$user = $this->get($this->filterUsername($query));
|
||||
} else {
|
||||
$user = parent::find($query, $field);
|
||||
}
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->load('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user account.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool True if user account was found and was deleted.
|
||||
*/
|
||||
public function delete($username): bool
|
||||
{
|
||||
$user = $this->load($username);
|
||||
|
||||
$exists = $user->exists();
|
||||
if ($exists) {
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function filterUsername(string $key)
|
||||
{
|
||||
$storage = $this->getFlexDirectory()->getStorage();
|
||||
if (method_exists($storage, 'normalizeKey')) {
|
||||
return $storage->normalizeKey($key);
|
||||
}
|
||||
|
||||
return mb_strtolower($key);
|
||||
}
|
||||
}
|
||||
204
system/src/Grav/Common/Flex/Types/Users/UserIndex.php
Normal file
204
system/src/Grav/Common/Flex/Types/Users/UserIndex.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users;
|
||||
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Flex\FlexIndex;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use Monolog\Logger;
|
||||
use function count;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class UserIndex
|
||||
* @package Grav\Common\Flex\Types\Users
|
||||
*
|
||||
* @extends FlexIndex<UserObject,UserCollection>
|
||||
*/
|
||||
class UserIndex extends FlexIndex implements UserCollectionInterface
|
||||
{
|
||||
public const VERSION = parent::VERSION . '.1';
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return array
|
||||
*/
|
||||
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
|
||||
{
|
||||
// Load saved index.
|
||||
$index = static::loadIndex($storage);
|
||||
|
||||
$version = $index['version'] ?? 0;
|
||||
$force = static::VERSION !== $version;
|
||||
|
||||
// TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found.
|
||||
//$timestamp = $index['timestamp'] ?? 0;
|
||||
//if (!$force && $timestamp && $timestamp > time() - 1) {
|
||||
// return $index['index'];
|
||||
//}
|
||||
|
||||
// Load up to date index.
|
||||
$entries = parent::loadEntriesFromStorage($storage);
|
||||
|
||||
return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $meta
|
||||
* @param array $data
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return void
|
||||
*/
|
||||
public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage)
|
||||
{
|
||||
// Username can also be number and stored as such.
|
||||
$key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
|
||||
$meta['key'] = static::filterUsername($key, $storage);
|
||||
$meta['email'] = isset($data['email']) ? mb_strtolower($data['email']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user account.
|
||||
*
|
||||
* Always creates user object. To check if user exists, use $this->exists().
|
||||
*
|
||||
* @param string $username
|
||||
* @return UserObject
|
||||
*/
|
||||
public function load($username): UserInterface
|
||||
{
|
||||
$username = (string)$username;
|
||||
|
||||
if ($username !== '') {
|
||||
$key = static::filterUsername($username, $this->getFlexDirectory()->getStorage());
|
||||
$user = $this->get($key);
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
} else {
|
||||
$key = '';
|
||||
}
|
||||
|
||||
$directory = $this->getFlexDirectory();
|
||||
|
||||
/** @var UserObject $object */
|
||||
$object = $directory->createObject(
|
||||
[
|
||||
'username' => $username,
|
||||
'state' => 'enabled'
|
||||
],
|
||||
$key
|
||||
);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user account.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool True if user account was found and was deleted.
|
||||
*/
|
||||
public function delete($username): bool
|
||||
{
|
||||
$user = $this->load($username);
|
||||
|
||||
$exists = $user->exists();
|
||||
if ($exists) {
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by username, email, etc
|
||||
*
|
||||
* @param string $query the query to search for
|
||||
* @param array $fields the fields to search
|
||||
* @return UserObject
|
||||
*/
|
||||
public function find($query, $fields = ['username', 'email']): UserInterface
|
||||
{
|
||||
if (is_string($query) && $query !== '') {
|
||||
foreach ((array)$fields as $field) {
|
||||
if ($field === 'key') {
|
||||
$user = $this->get($query);
|
||||
} elseif ($field === 'storage_key') {
|
||||
$user = $this->withKeyField('storage_key')->get($query);
|
||||
} elseif ($field === 'flex_key') {
|
||||
$user = $this->withKeyField('flex_key')->get($query);
|
||||
} elseif ($field === 'email') {
|
||||
$user = $this->withKeyField('email')->get($query);
|
||||
} elseif ($field === 'username') {
|
||||
$user = $this->get(static::filterUsername($query, $this->getFlexDirectory()->getStorage()));
|
||||
} else {
|
||||
$user = $this->__call('find', [$query, $field]);
|
||||
}
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->load('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return string
|
||||
*/
|
||||
protected static function filterUsername(string $key, FlexStorageInterface $storage): string
|
||||
{
|
||||
return $storage->normalizeKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return CompiledYamlFile|null
|
||||
*/
|
||||
protected static function getIndexFile(FlexStorageInterface $storage)
|
||||
{
|
||||
// Load saved index file.
|
||||
$grav = Grav::instance();
|
||||
$locator = $grav['locator'];
|
||||
$filename = $locator->findResource('user-data://flex/indexes/accounts.yaml', true, true);
|
||||
|
||||
return CompiledYamlFile::instance($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param array $added
|
||||
* @param array $updated
|
||||
* @param array $removed
|
||||
*/
|
||||
protected static function onChanges(array $entries, array $added, array $updated, array $removed)
|
||||
{
|
||||
$message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed));
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Logger $logger */
|
||||
$logger = $grav['log'];
|
||||
$logger->addDebug($message);
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addMessage($message, 'debug');
|
||||
}
|
||||
}
|
||||
909
system/src/Grav/Common/Flex/Types/Users/UserObject.php
Normal file
909
system/src/Grav/Common/Flex/Types/Users/UserObject.php
Normal file
@@ -0,0 +1,909 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Flex\Types\Users;
|
||||
|
||||
use Countable;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Flex\FlexObject;
|
||||
use Grav\Common\Flex\Traits\FlexGravTrait;
|
||||
use Grav\Common\Flex\Traits\FlexObjectTrait;
|
||||
use Grav\Common\Flex\Types\Users\Traits\UserObjectLegacyTrait;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
||||
use Grav\Common\Media\Interfaces\MediaUploadInterface;
|
||||
use Grav\Common\Page\Media;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
use Grav\Common\User\Access;
|
||||
use Grav\Common\User\Authentication;
|
||||
use Grav\Common\Flex\Types\UserGroups\UserGroupCollection;
|
||||
use Grav\Common\Flex\Types\UserGroups\UserGroupIndex;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\User\Traits\UserTrait;
|
||||
use Grav\Framework\File\Formatter\JsonFormatter;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
use Grav\Framework\Flex\Flex;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Flex\Storage\FileStorage;
|
||||
use Grav\Framework\Flex\Traits\FlexMediaTrait;
|
||||
use Grav\Framework\Form\FormFlashFile;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Flex User
|
||||
*
|
||||
* Flex User is mostly compatible with the older User class, except on few key areas:
|
||||
*
|
||||
* - Constructor parameters have been changed. Old code creating a new user does not work.
|
||||
* - Serializer has been changed -- existing sessions will be killed.
|
||||
*
|
||||
* @package Grav\Common\User
|
||||
*
|
||||
* @property string $username
|
||||
* @property string $email
|
||||
* @property string $fullname
|
||||
* @property string $state
|
||||
* @property array $groups
|
||||
* @property array $access
|
||||
* @property bool $authenticated
|
||||
* @property bool $authorized
|
||||
*/
|
||||
class UserObject extends FlexObject implements UserInterface, Countable
|
||||
{
|
||||
use FlexGravTrait;
|
||||
use FlexObjectTrait;
|
||||
use FlexMediaTrait {
|
||||
getMedia as private getFlexMedia;
|
||||
getMediaFolder as private getFlexMediaFolder;
|
||||
}
|
||||
use UserTrait;
|
||||
use UserObjectLegacyTrait;
|
||||
|
||||
/** @var array|null */
|
||||
protected $_uploads_original;
|
||||
/** @var FileInterface|null */
|
||||
protected $_storage;
|
||||
/** @var UserGroupIndex */
|
||||
protected $_groups;
|
||||
/** @var Access */
|
||||
protected $_access;
|
||||
/** @var array|null */
|
||||
protected $access;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'authorize' => 'session',
|
||||
'load' => false,
|
||||
'find' => false,
|
||||
'remove' => false,
|
||||
'get' => true,
|
||||
'set' => false,
|
||||
'undef' => false,
|
||||
'def' => false,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* UserObject constructor.
|
||||
* @param array $elements
|
||||
* @param string $key
|
||||
* @param FlexDirectory $directory
|
||||
* @param bool $validate
|
||||
*/
|
||||
public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
|
||||
{
|
||||
// User can only be authenticated via login.
|
||||
unset($elements['authenticated'], $elements['authorized']);
|
||||
|
||||
// Define username if it's not set.
|
||||
if (!isset($elements['username'])) {
|
||||
$storageKey = $elements['__META']['storage_key'] ?? null;
|
||||
if (null !== $storageKey && $key === $directory->getStorage()->normalizeKey($storageKey)) {
|
||||
$elements['username'] = $storageKey;
|
||||
} else {
|
||||
$elements['username'] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
// Define state if it isn't set.
|
||||
if (!isset($elements['state'])) {
|
||||
$elements['state'] = 'enabled';
|
||||
}
|
||||
|
||||
parent::__construct($elements, $key, $directory, $validate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function onPrepareRegistration(): void
|
||||
{
|
||||
if (!$this->getProperty('access')) {
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
$groups = $config->get('plugins.login.user_registration.groups', '');
|
||||
$access = $config->get('plugins.login.user_registration.access', ['site' => ['login' => true]]);
|
||||
|
||||
$this->setProperty('groups', $groups);
|
||||
$this->setProperty('access', $access);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get content editor will fall back if not set
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentEditor(): string
|
||||
{
|
||||
return $this->getProperty('content_editor', 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return $this->getNestedProperty($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @example $data->set('this.is.my.nested.variable', $value);
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $value New value.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function set($name, $value, $separator = null)
|
||||
{
|
||||
$this->setNestedProperty($name, $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @example $data->undef('this.is.my.nested.variable');
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function undef($name, $separator = null)
|
||||
{
|
||||
$this->unsetNestedProperty($name, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @example $data->def('this.is.my.nested.variable', 'default');
|
||||
*
|
||||
* @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 $this
|
||||
*/
|
||||
public function def($name, $default = null, $separator = null)
|
||||
{
|
||||
$this->defNestedProperty($name, $default, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @return bool|null
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null): ?bool
|
||||
{
|
||||
if ($scope === 'test') {
|
||||
// Special scope to test user permissions.
|
||||
$scope = null;
|
||||
} else {
|
||||
// User needs to be enabled.
|
||||
if ($this->getProperty('state') !== 'enabled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User needs to be logged in.
|
||||
if (!$this->getProperty('authenticated')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($action, 'login') === false && !$this->getProperty('authorized')) {
|
||||
// User needs to be authorized (2FA).
|
||||
return false;
|
||||
}
|
||||
|
||||
// Workaround bug in Login::isUserAuthorizedForPage() <= Login v3.0.4
|
||||
if ((string)(int)$action === $action) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check user access.
|
||||
$access = $this->getAccess();
|
||||
$authorized = $access->authorize($action, $scope);
|
||||
if (is_bool($authorized)) {
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
// If specific rule isn't hit, check if user is super user.
|
||||
if ($access->authorize('admin.super') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check group access.
|
||||
return $this->getGroups()->authorize($action, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProperty($property, $default = null)
|
||||
{
|
||||
$value = parent::getProperty($property, $default);
|
||||
|
||||
if ($property === 'avatar') {
|
||||
$settings = $this->getMediaFieldSettings($property);
|
||||
$value = $this->parseFileProperty($value, $settings);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object into an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$array = $this->jsonSerialize();
|
||||
|
||||
$settings = $this->getMediaFieldSettings('avatar');
|
||||
$array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null, $settings);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object into YAML string.
|
||||
*
|
||||
* @param int $inline The level where you switch to inline YAML.
|
||||
* @param int $indent The amount of spaces to use for indentation of nested nodes.
|
||||
* @return string A YAML string representing the object.
|
||||
*/
|
||||
public function toYaml($inline = 5, $indent = 2)
|
||||
{
|
||||
$yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]);
|
||||
|
||||
return $yaml->encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object into JSON string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson()
|
||||
{
|
||||
$json = new JsonFormatter();
|
||||
|
||||
return $json->encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Join nested values together by using blueprints.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $value Value to be joined.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function join($name, $value, $separator = null)
|
||||
{
|
||||
$separator = $separator ?? '.';
|
||||
$old = $this->get($name, null, $separator);
|
||||
if ($old !== null) {
|
||||
if (!is_array($old)) {
|
||||
throw new RuntimeException('Value ' . $old);
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
} elseif (!is_array($value)) {
|
||||
throw new RuntimeException('Value ' . $value);
|
||||
}
|
||||
|
||||
$value = $this->getBlueprint()->mergeData($old, $value, $name, $separator);
|
||||
}
|
||||
|
||||
$this->set($name, $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested structure containing default values defined in the blueprints.
|
||||
*
|
||||
* Fields without default value are ignored in the list.
|
||||
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaults()
|
||||
{
|
||||
return $this->getBlueprint()->getDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values by using blueprints.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $value Value to be joined.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function joinDefaults($name, $value, $separator = null)
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
$old = $this->get($name, null, $separator);
|
||||
if ($old !== null) {
|
||||
$value = $this->getBlueprint()->mergeData($value, $old, $name, $separator ?? '.');
|
||||
}
|
||||
|
||||
$this->setNestedProperty($name, $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from the configuration and join it with given data.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param array|object $value Value to be joined.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return array
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getJoined($name, $value, $separator = null)
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
} elseif (!is_array($value)) {
|
||||
throw new RuntimeException('Value ' . $value);
|
||||
}
|
||||
|
||||
$old = $this->get($name, null, $separator);
|
||||
|
||||
if ($old === null) {
|
||||
// No value set; no need to join data.
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!is_array($old)) {
|
||||
throw new RuntimeException('Value ' . $old);
|
||||
}
|
||||
|
||||
// Return joined data.
|
||||
return $this->getBlueprint()->mergeData($old, $value, $name, $separator ?? '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values to the configuration if variables were not set.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefaults(array $data)
|
||||
{
|
||||
$this->setElements($this->getBlueprint()->mergeData($data, $this->toArray()));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate by blueprints.
|
||||
*
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
$this->getBlueprint()->validate($this->toArray());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all items by using blueprints.
|
||||
* @return $this
|
||||
*/
|
||||
public function filter()
|
||||
{
|
||||
$this->setElements($this->getBlueprint()->filter($this->toArray()));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra items which haven't been defined in blueprints.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extra()
|
||||
{
|
||||
return $this->getBlueprint()->extra($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return unmodified data as raw string.
|
||||
*
|
||||
* NOTE: This function only returns data which has been saved to the storage.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function raw()
|
||||
{
|
||||
$file = $this->file();
|
||||
|
||||
return $file ? $file->raw() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or get the data storage.
|
||||
*
|
||||
* @param FileInterface|null $storage Optionally enter a new storage.
|
||||
* @return FileInterface|null
|
||||
*/
|
||||
public function file(FileInterface $storage = null)
|
||||
{
|
||||
if (null !== $storage) {
|
||||
$this->_storage = $storage;
|
||||
}
|
||||
|
||||
return $this->_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->getProperty('state') !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
// TODO: We may want to handle this in the storage layer in the future.
|
||||
$key = $this->getStorageKey();
|
||||
if (!$key || strpos($key, '@@')) {
|
||||
$storage = $this->getFlexDirectory()->getStorage();
|
||||
if ($storage instanceof FileStorage) {
|
||||
$this->setStorageKey($this->getKey());
|
||||
}
|
||||
}
|
||||
|
||||
$password = $this->getProperty('password') ?? $this->getProperty('password1');
|
||||
if (null !== $password && '' !== $password) {
|
||||
$password2 = $this->getProperty('password2');
|
||||
if (!\is_string($password) || ($password2 && $password !== $password2)) {
|
||||
throw new \RuntimeException('Passwords did not match.');
|
||||
}
|
||||
|
||||
$this->setProperty('hashed_password', Authentication::create($password));
|
||||
}
|
||||
$this->unsetProperty('password');
|
||||
$this->unsetProperty('password1');
|
||||
$this->unsetProperty('password2');
|
||||
|
||||
// Backwards compatibility with older plugins.
|
||||
$fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
|
||||
$grav = $this->getContainer();
|
||||
if ($fireEvents) {
|
||||
$self = $this;
|
||||
$grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
|
||||
if ($self !== $this) {
|
||||
throw new RuntimeException('Switching Flex User object during onAdminSave event is not supported! Please update plugin.');
|
||||
}
|
||||
}
|
||||
|
||||
$instance = parent::save();
|
||||
|
||||
// Backwards compatibility with older plugins.
|
||||
if ($fireEvents) {
|
||||
$grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function prepareStorage(): array
|
||||
{
|
||||
$elements = parent::prepareStorage();
|
||||
|
||||
// Do not save authorization information.
|
||||
unset($elements['authenticated'], $elements['authorized']);
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MediaCollectionInterface
|
||||
*/
|
||||
public function getMedia()
|
||||
{
|
||||
/** @var Media $media */
|
||||
$media = $this->getFlexMedia();
|
||||
|
||||
// Deal with shared avatar folder.
|
||||
$path = $this->getAvatarFile();
|
||||
if ($path && !$media[$path] && is_file($path)) {
|
||||
$medium = MediumFactory::fromFile($path);
|
||||
if ($medium) {
|
||||
$media->add($path, $medium);
|
||||
$name = basename($path);
|
||||
if ($name !== $path) {
|
||||
$media->add($name, $medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMediaFolder(): ?string
|
||||
{
|
||||
$folder = $this->getFlexMediaFolder();
|
||||
|
||||
// Check for shared media
|
||||
if (!$folder && !$this->getFlexDirectory()->getMediaFolder()) {
|
||||
$this->_loadMedia = false;
|
||||
$folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'user://accounts/avatars';
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Blueprint
|
||||
*/
|
||||
protected function doGetBlueprint(string $name = ''): Blueprint
|
||||
{
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint($name ? '.' . $name : $name);
|
||||
|
||||
// HACK: With folder storage we need to ignore the avatar destination.
|
||||
if ($this->getFlexDirectory()->getMediaFolder()) {
|
||||
$field = $blueprint->get('form/fields/avatar');
|
||||
if ($field) {
|
||||
unset($field['destination']);
|
||||
$blueprint->set('form/fields/avatar', $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe = false): ?bool
|
||||
{
|
||||
if ($user instanceof self && $user->getStorageKey() === $this->getStorageKey()) {
|
||||
// User cannot delete his own account, otherwise he has full access.
|
||||
return $action !== 'delete';
|
||||
}
|
||||
|
||||
return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getAvatarFile(): ?string
|
||||
{
|
||||
$avatars = $this->getElement('avatar');
|
||||
if (is_array($avatars) && $avatars) {
|
||||
$avatar = array_shift($avatars);
|
||||
|
||||
return $avatar['path'] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the associated media collection (original images).
|
||||
*
|
||||
* @return MediaCollectionInterface Representation of associated media.
|
||||
*/
|
||||
protected function getOriginalMedia()
|
||||
{
|
||||
$folder = $this->getMediaFolder();
|
||||
if ($folder) {
|
||||
$folder .= '/original';
|
||||
}
|
||||
|
||||
return (new Media($folder ?? '', $this->getMediaOrder()))->setTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
*/
|
||||
protected function setUpdatedMedia(array $files): void
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$list = [];
|
||||
$list_original = [];
|
||||
foreach ($files as $field => $group) {
|
||||
if ($field === '') {
|
||||
continue;
|
||||
}
|
||||
$field = (string)$field;
|
||||
|
||||
// Load settings for the field.
|
||||
$settings = $this->getMediaFieldSettings($field);
|
||||
foreach ($group as $filename => $file) {
|
||||
if ($file) {
|
||||
// File upload.
|
||||
$filename = $file->getClientFilename();
|
||||
|
||||
/** @var FormFlashFile $file */
|
||||
$data = $file->jsonSerialize();
|
||||
unset($data['tmp_name'], $data['path']);
|
||||
} else {
|
||||
// File delete.
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if ($file) {
|
||||
// Check file upload against media limits.
|
||||
$filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
|
||||
}
|
||||
|
||||
$self = $settings['self'];
|
||||
if ($this->_loadMedia && $self) {
|
||||
$filepath = $filename;
|
||||
} else {
|
||||
$filepath = "{$settings['destination']}/{$filename}";
|
||||
|
||||
// For backwards compatibility we are always using relative path from the installation root.
|
||||
if ($locator->isStream($filepath)) {
|
||||
$filepath = $locator->findResource($filepath, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for original images.
|
||||
if (strpos($field, '/original')) {
|
||||
if ($this->_loadMedia && $self) {
|
||||
$list_original[$filename] = [$file, $settings];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$filename] = [$file, $settings];
|
||||
|
||||
if (null !== $data) {
|
||||
$data['name'] = $filename;
|
||||
$data['path'] = $filepath;
|
||||
|
||||
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
|
||||
} else {
|
||||
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearMediaCache();
|
||||
|
||||
$this->_uploads = $list;
|
||||
$this->_uploads_original = $list_original;
|
||||
}
|
||||
|
||||
protected function saveUpdatedMedia(): void
|
||||
{
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
throw new RuntimeException('Internal error UO101');
|
||||
}
|
||||
|
||||
// Upload/delete original sized images.
|
||||
/**
|
||||
* @var string $filename
|
||||
* @var UploadedFileInterface|array|null $file
|
||||
*/
|
||||
foreach ($this->_uploads_original ?? [] as $filename => $file) {
|
||||
$filename = 'original/' . $filename;
|
||||
if (is_array($file)) {
|
||||
[$file, $settings] = $file;
|
||||
} else {
|
||||
$settings = null;
|
||||
}
|
||||
if ($file instanceof UploadedFileInterface) {
|
||||
$media->copyUploadedFile($file, $filename, $settings);
|
||||
} else {
|
||||
$media->deleteFile($filename, $settings);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload/delete altered files.
|
||||
/**
|
||||
* @var string $filename
|
||||
* @var UploadedFileInterface|array|null $file
|
||||
*/
|
||||
foreach ($this->getUpdatedMedia() as $filename => $file) {
|
||||
if (is_array($file)) {
|
||||
[$file, $settings] = $file;
|
||||
} else {
|
||||
$settings = null;
|
||||
}
|
||||
if ($file instanceof UploadedFileInterface) {
|
||||
$media->copyUploadedFile($file, $filename, $settings);
|
||||
} else {
|
||||
$media->deleteFile($filename, $settings);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setUpdatedMedia([]);
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getFlexType(),
|
||||
'key' => $this->getKey(),
|
||||
'elements' => $this->jsonSerialize(),
|
||||
'storage' => $this->getMetaData()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserGroupIndex
|
||||
*/
|
||||
protected function getUserGroups()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Flex $flex */
|
||||
$flex = $grav['flex'];
|
||||
|
||||
/** @var UserGroupCollection|null $groups */
|
||||
$groups = $flex->getDirectory('user-groups');
|
||||
if ($groups) {
|
||||
/** @var UserGroupIndex $index */
|
||||
$index = $groups->getIndex();
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
return $grav['user_groups'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserGroupIndex
|
||||
*/
|
||||
protected function getGroups()
|
||||
{
|
||||
if (null === $this->_groups) {
|
||||
$this->_groups = $this->getUserGroups()->select((array)$this->getProperty('groups'));
|
||||
}
|
||||
|
||||
return $this->_groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Access
|
||||
*/
|
||||
protected function getAccess(): Access
|
||||
{
|
||||
if (null === $this->_access) {
|
||||
$this->getProperty('access');
|
||||
}
|
||||
|
||||
return $this->_access;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetLoad_access($value): array
|
||||
{
|
||||
if (!$value instanceof Access) {
|
||||
$value = new Access($value);
|
||||
}
|
||||
|
||||
$this->_access = $value;
|
||||
|
||||
return $value->jsonSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetPrepare_access($value): array
|
||||
{
|
||||
return $this->offsetLoad_access($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $value
|
||||
* @return array|null
|
||||
*/
|
||||
protected function offsetSerialize_access(?array $value): ?array
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
106
system/src/Grav/Common/Form/FormFlash.php
Normal file
106
system/src/Grav/Common/Form/FormFlash.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Form;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Framework\Form\FormFlash as FrameworkFormFlash;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class FormFlash
|
||||
* @package Grav\Common\Form
|
||||
*/
|
||||
class FormFlash extends FrameworkFormFlash
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
* @deprecated 1.6 For backwards compatibility only, do not use
|
||||
*/
|
||||
public function getLegacyFiles(): array
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($this->files as $field => $files) {
|
||||
if (strpos($field, '/')) {
|
||||
continue;
|
||||
}
|
||||
foreach ($files as $file) {
|
||||
if (is_array($file)) {
|
||||
$file['tmp_name'] = $this->getTmpDir() . '/' . $file['tmp_name'];
|
||||
$fields[$field][$file['path'] ?? $file['name']] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $filename
|
||||
* @param array $upload
|
||||
* @return bool
|
||||
* @deprecated 1.6 For backwards compatibility only, do not use
|
||||
*/
|
||||
public function uploadFile(string $field, string $filename, array $upload): bool
|
||||
{
|
||||
if (!$this->uniqueId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tmp_dir = $this->getTmpDir();
|
||||
Folder::create($tmp_dir);
|
||||
|
||||
$tmp_file = $upload['file']['tmp_name'];
|
||||
$basename = basename($tmp_file);
|
||||
|
||||
if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$upload['file']['tmp_name'] = $basename;
|
||||
$upload['file']['name'] = $filename;
|
||||
|
||||
$this->addFileInternal($field, $filename, $upload['file']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $filename
|
||||
* @param array $upload
|
||||
* @param array $crop
|
||||
* @return bool
|
||||
* @deprecated 1.6 For backwards compatibility only, do not use
|
||||
*/
|
||||
public function cropFile(string $field, string $filename, array $upload, array $crop): bool
|
||||
{
|
||||
if (!$this->uniqueId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tmp_dir = $this->getTmpDir();
|
||||
Folder::create($tmp_dir);
|
||||
|
||||
$tmp_file = $upload['file']['tmp_name'];
|
||||
$basename = basename($tmp_file);
|
||||
|
||||
if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$upload['file']['tmp_name'] = $basename;
|
||||
$upload['file']['name'] = $filename;
|
||||
|
||||
$this->addFileInternal($field, $filename, $upload['file'], $crop);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,19 +11,23 @@ namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
/**
|
||||
* Class AbstractCollection
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
abstract class AbstractCollection extends Iterator
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toJson()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return json_encode($items);
|
||||
return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,10 +11,18 @@ namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
/**
|
||||
* Class AbstractPackageCollection
|
||||
* @package Grav\Common\GPM\Common
|
||||
*/
|
||||
abstract class AbstractPackageCollection extends Iterator
|
||||
{
|
||||
/** @var string */
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toJson()
|
||||
{
|
||||
$items = [];
|
||||
@@ -22,9 +31,12 @@ abstract class AbstractPackageCollection extends Iterator
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return json_encode($items);
|
||||
return json_encode($items, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,18 +11,32 @@ namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
class CachedCollection extends Iterator {
|
||||
|
||||
protected static $cache;
|
||||
/**
|
||||
* Class CachedCollection
|
||||
* @package Grav\Common\GPM\Common
|
||||
*/
|
||||
class CachedCollection extends Iterator
|
||||
{
|
||||
/** @var array */
|
||||
protected static $cache = [];
|
||||
|
||||
/**
|
||||
* CachedCollection constructor.
|
||||
*
|
||||
* @param array $items
|
||||
*/
|
||||
public function __construct($items)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$method = static::class . __METHOD__;
|
||||
|
||||
// local cache to speed things up
|
||||
if (!isset(self::$cache[get_called_class().__METHOD__])) {
|
||||
self::$cache[get_called_class().__METHOD__] = $items;
|
||||
if (!isset(self::$cache[$method])) {
|
||||
self::$cache[$method] = $items;
|
||||
}
|
||||
|
||||
foreach (self::$cache[get_called_class().__METHOD__] as $name => $item) {
|
||||
foreach (self::$cache[$method] as $name => $item) {
|
||||
$this->append([$name => $item]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,11 +11,21 @@ namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
|
||||
class Package {
|
||||
|
||||
/**
|
||||
* @property string $name
|
||||
*/
|
||||
class Package
|
||||
{
|
||||
/** @var Data */
|
||||
protected $data;
|
||||
|
||||
public function __construct(Data $package, $type = null) {
|
||||
/**
|
||||
* Package constructor.
|
||||
* @param Data $package
|
||||
* @param string|null $type
|
||||
*/
|
||||
public function __construct(Data $package, $type = null)
|
||||
{
|
||||
$this->data = $package;
|
||||
|
||||
if ($type) {
|
||||
@@ -22,28 +33,63 @@ class Package {
|
||||
}
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
/**
|
||||
* @return Data
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->data->get($key);
|
||||
}
|
||||
|
||||
public function __isset($key) {
|
||||
return isset($this->data->$key);
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function __set($key, $value)
|
||||
{
|
||||
$this->data->set($key, $value);
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($key)
|
||||
{
|
||||
return isset($this->data->{$key});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toJson();
|
||||
}
|
||||
|
||||
public function toJson() {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toJson()
|
||||
{
|
||||
return $this->data->toJson();
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->data->toArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class Installer
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Installer
|
||||
{
|
||||
/** @const No error */
|
||||
const OK = 0;
|
||||
public const OK = 0;
|
||||
/** @const Target already exists */
|
||||
const EXISTS = 1;
|
||||
public const EXISTS = 1;
|
||||
/** @const Target is a symbolic link */
|
||||
const IS_LINK = 2;
|
||||
public const IS_LINK = 2;
|
||||
/** @const Target doesn't exist */
|
||||
const NOT_FOUND = 4;
|
||||
public const NOT_FOUND = 4;
|
||||
/** @const Target is not a directory */
|
||||
const NOT_DIRECTORY = 8;
|
||||
public const NOT_DIRECTORY = 8;
|
||||
/** @const Target is not a Grav instance */
|
||||
const NOT_GRAV_ROOT = 16;
|
||||
public const NOT_GRAV_ROOT = 16;
|
||||
/** @const Error while trying to open the ZIP package */
|
||||
const ZIP_OPEN_ERROR = 32;
|
||||
public const ZIP_OPEN_ERROR = 32;
|
||||
/** @const Error while trying to extract the ZIP package */
|
||||
const ZIP_EXTRACT_ERROR = 64;
|
||||
public const ZIP_EXTRACT_ERROR = 64;
|
||||
/** @const Invalid source file */
|
||||
const INVALID_SOURCE = 128;
|
||||
public const INVALID_SOURCE = 128;
|
||||
|
||||
/**
|
||||
* Destination folder on which validation checks are applied
|
||||
* @var string
|
||||
*/
|
||||
/** @var string Destination folder on which validation checks are applied */
|
||||
protected static $target;
|
||||
|
||||
/**
|
||||
* @var integer Error Code
|
||||
*/
|
||||
/** @var int|string Error code or string */
|
||||
protected static $error = 0;
|
||||
|
||||
/**
|
||||
* @var integer Zip Error Code
|
||||
*/
|
||||
/** @var int Zip Error Code */
|
||||
protected static $error_zip = 0;
|
||||
|
||||
/**
|
||||
* @var string Post install message
|
||||
*/
|
||||
/** @var string Post install message */
|
||||
protected static $message = '';
|
||||
|
||||
/**
|
||||
* Default options for the install
|
||||
* @var array
|
||||
*/
|
||||
/** @var array Default options for the install */
|
||||
protected static $options = [
|
||||
'overwrite' => true,
|
||||
'ignore_symlinks' => true,
|
||||
@@ -73,30 +72,33 @@ class Installer
|
||||
* @param string $zip the local path to ZIP package
|
||||
* @param string $destination The local path to the Grav Instance
|
||||
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
|
||||
* @param string $extracted The local path to the extacted ZIP package
|
||||
* @param string|null $extracted The local path to the extacted ZIP package
|
||||
* @param bool $keepExtracted True if you want to keep the original files
|
||||
* @return bool True if everything went fine, False otherwise.
|
||||
*/
|
||||
public static function install($zip, $destination, $options = [], $extracted = null)
|
||||
public static function install($zip, $destination, $options = [], $extracted = null, $keepExtracted = false)
|
||||
{
|
||||
$destination = rtrim($destination, DS);
|
||||
$options = array_merge(self::$options, $options);
|
||||
$install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS);
|
||||
|
||||
if (!self::isGravInstance($destination) || !self::isValidDestination($install_path,
|
||||
$options['exclude_checks'])
|
||||
if (!self::isGravInstance($destination) || !self::isValidDestination(
|
||||
$install_path,
|
||||
$options['exclude_checks']
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] ||
|
||||
self::lastErrorCode() == self::EXISTS && !$options['overwrite']
|
||||
if ((self::lastErrorCode() === self::IS_LINK && $options['ignore_symlinks']) ||
|
||||
(self::lastErrorCode() === self::EXISTS && !$options['overwrite'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a tmp location
|
||||
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
|
||||
$tmp = $tmp_dir . '/Grav-' . uniqid();
|
||||
$tmp = $tmp_dir . '/Grav-' . uniqid('', false);
|
||||
|
||||
if (!$extracted) {
|
||||
$extracted = self::unZip($zip, $tmp);
|
||||
@@ -111,7 +113,6 @@ class Installer
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$is_install = true;
|
||||
$installer = self::loadInstaller($extracted, $is_install);
|
||||
|
||||
@@ -134,13 +135,16 @@ class Installer
|
||||
}
|
||||
|
||||
if (!$options['sophisticated']) {
|
||||
if ($options['theme']) {
|
||||
$isTheme = $options['theme'] ?? false;
|
||||
// Make sure that themes are always being copied, even if option was not set!
|
||||
$isTheme = $isTheme || preg_match('|/themes/[^/]+|ui', $install_path);
|
||||
if ($isTheme) {
|
||||
self::copyInstall($extracted, $install_path);
|
||||
} else {
|
||||
self::moveInstall($extracted, $install_path);
|
||||
}
|
||||
} else {
|
||||
self::sophisticatedInstall($extracted, $install_path, $options['ignores']);
|
||||
self::sophisticatedInstall($extracted, $install_path, $options['ignores'], $keepExtracted);
|
||||
}
|
||||
|
||||
Folder::delete($tmp);
|
||||
@@ -159,23 +163,22 @@ class Installer
|
||||
self::$error = self::OK;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip a file to somewhere
|
||||
*
|
||||
* @param $zip_file
|
||||
* @param $destination
|
||||
* @return bool|string
|
||||
* @param string $zip_file
|
||||
* @param string $destination
|
||||
* @return string|false
|
||||
*/
|
||||
public static function unZip($zip_file, $destination)
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
$zip = new ZipArchive();
|
||||
$archive = $zip->open($zip_file);
|
||||
|
||||
if ($archive === true) {
|
||||
Folder::mkdir($destination);
|
||||
Folder::create($destination);
|
||||
|
||||
$unzip = $zip->extractTo($destination);
|
||||
|
||||
@@ -187,15 +190,19 @@ class Installer
|
||||
return false;
|
||||
}
|
||||
|
||||
$package_folder_name = preg_replace('#\./$#', '', $zip->getNameIndex(0));
|
||||
$package_folder_name = $zip->getNameIndex(0);
|
||||
if ($package_folder_name === false) {
|
||||
throw new \RuntimeException('Bad package file: ' . basename($zip_file));
|
||||
}
|
||||
$package_folder_name = preg_replace('#\./$#', '', $package_folder_name);
|
||||
$zip->close();
|
||||
$extracted_folder = $destination . '/' . $package_folder_name;
|
||||
|
||||
return $extracted_folder;
|
||||
return $destination . '/' . $package_folder_name;
|
||||
}
|
||||
|
||||
self::$error = self::ZIP_EXTRACT_ERROR;
|
||||
self::$error_zip = $archive;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -204,23 +211,20 @@ class Installer
|
||||
*
|
||||
* @param string $installer_file_folder The folder path that contains install.php
|
||||
* @param bool $is_install True if install, false if removal
|
||||
*
|
||||
* @return null|string
|
||||
* @return string|null
|
||||
*/
|
||||
private static function loadInstaller($installer_file_folder, $is_install)
|
||||
{
|
||||
$installer = null;
|
||||
|
||||
$installer_file_folder = rtrim($installer_file_folder, DS);
|
||||
|
||||
$install_file = $installer_file_folder . DS . 'install.php';
|
||||
|
||||
if (file_exists($install_file)) {
|
||||
require_once($install_file);
|
||||
} else {
|
||||
if (!file_exists($install_file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
require_once $install_file;
|
||||
|
||||
if ($is_install) {
|
||||
$slug = '';
|
||||
if (($pos = strpos($installer_file_folder, 'grav-plugin-')) !== false) {
|
||||
@@ -243,19 +247,18 @@ class Installer
|
||||
return $class_name;
|
||||
}
|
||||
|
||||
$class_name_alphanumeric = preg_replace('/[^a-zA-Z0-9]+/', '', $class_name);
|
||||
$class_name_alphanumeric = preg_replace('/[^a-zA-Z0-9]+/', '', $class_name) ?? $class_name;
|
||||
|
||||
if (class_exists($class_name_alphanumeric)) {
|
||||
return $class_name_alphanumeric;
|
||||
}
|
||||
|
||||
return $installer;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $source_path
|
||||
* @param $install_path
|
||||
*
|
||||
* @param string $source_path
|
||||
* @param string $install_path
|
||||
* @return bool
|
||||
*/
|
||||
public static function moveInstall($source_path, $install_path)
|
||||
@@ -270,33 +273,32 @@ class Installer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $source_path
|
||||
* @param $install_path
|
||||
*
|
||||
* @param string $source_path
|
||||
* @param string $install_path
|
||||
* @return bool
|
||||
*/
|
||||
public static function copyInstall($source_path, $install_path)
|
||||
{
|
||||
if (empty($source_path)) {
|
||||
throw new \RuntimeException("Directory $source_path is missing");
|
||||
} else {
|
||||
Folder::rcopy($source_path, $install_path);
|
||||
throw new RuntimeException("Directory $source_path is missing");
|
||||
}
|
||||
|
||||
Folder::rcopy($source_path, $install_path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $source_path
|
||||
* @param $install_path
|
||||
*
|
||||
* @param string $source_path
|
||||
* @param string $install_path
|
||||
* @param array $ignores
|
||||
* @param bool $keep_source
|
||||
* @return bool
|
||||
*/
|
||||
public static function sophisticatedInstall($source_path, $install_path, $ignores = [])
|
||||
public static function sophisticatedInstall($source_path, $install_path, $ignores = [], $keep_source = false)
|
||||
{
|
||||
foreach (new \DirectoryIterator($source_path) as $file) {
|
||||
|
||||
if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores)) {
|
||||
foreach (new DirectoryIterator($source_path) as $file) {
|
||||
if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -304,10 +306,15 @@ class Installer
|
||||
|
||||
if ($file->isDir()) {
|
||||
Folder::delete($path);
|
||||
Folder::move($file->getPathname(), $path);
|
||||
if ($keep_source) {
|
||||
Folder::copy($file->getPathname(), $path);
|
||||
} else {
|
||||
Folder::move($file->getPathname(), $path);
|
||||
}
|
||||
|
||||
if ($file->getFilename() === 'bin') {
|
||||
foreach (glob($path . DS . '*') as $bin_file) {
|
||||
$glob = glob($path . DS . '*') ?: [];
|
||||
foreach ($glob as $bin_file) {
|
||||
@chmod($bin_file, 0755);
|
||||
}
|
||||
}
|
||||
@@ -325,8 +332,7 @@ class Installer
|
||||
*
|
||||
* @param string $path The slug of the package(s)
|
||||
* @param array $options Options to use for uninstalling
|
||||
*
|
||||
* @return boolean True if everything went fine, False otherwise.
|
||||
* @return bool True if everything went fine, False otherwise.
|
||||
*/
|
||||
public static function uninstall($path, $options = [])
|
||||
{
|
||||
@@ -367,8 +373,7 @@ class Installer
|
||||
*
|
||||
* @param string $destination The directory to run validations at
|
||||
* @param array $exclude An array of constants to exclude from the validation
|
||||
*
|
||||
* @return boolean True if validation passed. False otherwise
|
||||
* @return bool True if validation passed. False otherwise
|
||||
*/
|
||||
public static function isValidDestination($destination, $exclude = [])
|
||||
{
|
||||
@@ -385,27 +390,25 @@ class Installer
|
||||
self::$error = self::NOT_DIRECTORY;
|
||||
}
|
||||
|
||||
if (count($exclude) && in_array(self::$error, $exclude)) {
|
||||
if (count($exclude) && in_array(self::$error, $exclude, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(self::$error);
|
||||
return !self::$error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the given path is a Grav Instance
|
||||
*
|
||||
* @param string $target The local path to the Grav Instance
|
||||
*
|
||||
* @return boolean True if is a Grav Instance. False otherwise
|
||||
* @return bool True if is a Grav Instance. False otherwise
|
||||
*/
|
||||
public static function isGravInstance($target)
|
||||
{
|
||||
self::$error = 0;
|
||||
self::$target = $target;
|
||||
|
||||
if (
|
||||
!file_exists($target . DS . 'index.php') ||
|
||||
if (!file_exists($target . DS . 'index.php') ||
|
||||
!file_exists($target . DS . 'bin') ||
|
||||
!file_exists($target . DS . 'user') ||
|
||||
!file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml')
|
||||
@@ -418,6 +421,7 @@ class Installer
|
||||
|
||||
/**
|
||||
* Returns the last message added by the installer
|
||||
*
|
||||
* @return string The message
|
||||
*/
|
||||
public static function getMessage()
|
||||
@@ -427,6 +431,7 @@ class Installer
|
||||
|
||||
/**
|
||||
* Returns the last error occurred in a string message format
|
||||
*
|
||||
* @return string The message of the last error
|
||||
*/
|
||||
public static function lastErrorMsg()
|
||||
@@ -467,42 +472,46 @@ class Installer
|
||||
case self::ZIP_EXTRACT_ERROR:
|
||||
$msg = 'Unable to extract the package. ';
|
||||
if (self::$error_zip) {
|
||||
switch(self::$error_zip) {
|
||||
case \ZipArchive::ER_EXISTS:
|
||||
$msg .= "File already exists.";
|
||||
switch (self::$error_zip) {
|
||||
case ZipArchive::ER_EXISTS:
|
||||
$msg .= 'File already exists.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_INCONS:
|
||||
$msg .= "Zip archive inconsistent.";
|
||||
case ZipArchive::ER_INCONS:
|
||||
$msg .= 'Zip archive inconsistent.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_MEMORY:
|
||||
$msg .= "Malloc failure.";
|
||||
case ZipArchive::ER_MEMORY:
|
||||
$msg .= 'Memory allocation failure.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_NOENT:
|
||||
$msg .= "No such file.";
|
||||
case ZipArchive::ER_NOENT:
|
||||
$msg .= 'No such file.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_NOZIP:
|
||||
$msg .= "Not a zip archive.";
|
||||
case ZipArchive::ER_NOZIP:
|
||||
$msg .= 'Not a zip archive.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_OPEN:
|
||||
case ZipArchive::ER_OPEN:
|
||||
$msg .= "Can't open file.";
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_READ:
|
||||
$msg .= "Read error.";
|
||||
case ZipArchive::ER_READ:
|
||||
$msg .= 'Read error.';
|
||||
break;
|
||||
|
||||
case \ZipArchive::ER_SEEK:
|
||||
$msg .= "Seek error.";
|
||||
case ZipArchive::ER_SEEK:
|
||||
$msg .= 'Seek error.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case self::INVALID_SOURCE:
|
||||
$msg = 'Invalid source file';
|
||||
break;
|
||||
|
||||
default:
|
||||
$msg = 'Unknown Error';
|
||||
break;
|
||||
@@ -513,7 +522,8 @@ class Installer
|
||||
|
||||
/**
|
||||
* Returns the last error code of the occurred error
|
||||
* @return integer The code of the last error
|
||||
*
|
||||
* @return int|string The code of the last error
|
||||
*/
|
||||
public static function lastErrorCode()
|
||||
{
|
||||
@@ -524,8 +534,8 @@ class Installer
|
||||
* Allows to manually set an error
|
||||
*
|
||||
* @param int|string $error the Error code
|
||||
* @return void
|
||||
*/
|
||||
|
||||
public static function setError($error)
|
||||
{
|
||||
self::$error = $error;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,8 @@ namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class Licenses
|
||||
@@ -18,29 +21,22 @@ use Grav\Common\Grav;
|
||||
*/
|
||||
class Licenses
|
||||
{
|
||||
|
||||
/**
|
||||
* Regex to validate the format of a License
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
/** @var string Regex to validate the format of a License */
|
||||
protected static $regex = '^(?:[A-F0-9]{8}-){3}(?:[A-F0-9]{8}){1}$';
|
||||
|
||||
/** @var FileInterface */
|
||||
protected static $file;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the license for a Premium package
|
||||
*
|
||||
* @param $slug
|
||||
* @param $license
|
||||
*
|
||||
* @return boolean
|
||||
* @param string $slug
|
||||
* @param string $license
|
||||
* @return bool
|
||||
*/
|
||||
public static function set($slug, $license)
|
||||
{
|
||||
$licenses = self::getLicenseFile();
|
||||
$data = $licenses->content();
|
||||
$data = (array)$licenses->content();
|
||||
$slug = strtolower($slug);
|
||||
|
||||
if ($license && !self::validate($license)) {
|
||||
@@ -66,34 +62,29 @@ class Licenses
|
||||
/**
|
||||
* Returns the license for a Premium package
|
||||
*
|
||||
* @param $slug
|
||||
*
|
||||
* @return string
|
||||
* @param string|null $slug
|
||||
* @return string[]|string
|
||||
*/
|
||||
public static function get($slug = null)
|
||||
{
|
||||
$licenses = self::getLicenseFile();
|
||||
$data = $licenses->content();
|
||||
$data = (array)$licenses->content();
|
||||
$licenses->free();
|
||||
|
||||
if (null === $slug) {
|
||||
return $data['licenses'] ?? [];
|
||||
}
|
||||
|
||||
$slug = strtolower($slug);
|
||||
|
||||
if (!$slug) {
|
||||
return isset($data['licenses']) ? $data['licenses'] : [];
|
||||
}
|
||||
|
||||
if (!isset($data['licenses']) || !isset($data['licenses'][$slug])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $data['licenses'][$slug];
|
||||
return $data['licenses'][$slug] ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates the License format
|
||||
*
|
||||
* @param $license
|
||||
*
|
||||
* @param string|null $license
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate($license = null)
|
||||
@@ -102,19 +93,18 @@ class Licenses
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('#' . self::$regex. '#', $license);
|
||||
return (bool)preg_match('#' . self::$regex. '#', $license);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's the License File object
|
||||
* Get the License File object
|
||||
*
|
||||
* @return \RocketTheme\Toolbox\File\FileInterface
|
||||
* @return FileInterface
|
||||
*/
|
||||
public static function getLicenseFile()
|
||||
|
||||
{
|
||||
if (!isset(self::$file)) {
|
||||
$path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';
|
||||
$path = Grav::instance()['locator']->findResource('user-data://') . '/licenses.yaml';
|
||||
if (!file_exists($path)) {
|
||||
touch($path);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,10 +11,21 @@ namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
|
||||
|
||||
/**
|
||||
* Class AbstractPackageCollection
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
abstract class AbstractPackageCollection extends BaseCollection
|
||||
{
|
||||
/**
|
||||
* AbstractPackageCollection constructor.
|
||||
*
|
||||
* @param array $items
|
||||
*/
|
||||
public function __construct($items)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
foreach ($items as $name => $data) {
|
||||
$data->set('slug', $name);
|
||||
$this->items[$name] = new Package($data, $this->type);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,11 +11,22 @@ namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\GPM\Common\Package as BasePackage;
|
||||
use Parsedown;
|
||||
|
||||
/**
|
||||
* Class Package
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Package extends BasePackage
|
||||
{
|
||||
/** @var array */
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* Package constructor.
|
||||
* @param Data $package
|
||||
* @param string|null $package_type
|
||||
*/
|
||||
public function __construct(Data $package, $package_type = null)
|
||||
{
|
||||
$data = new Data($package->blueprints()->toArray());
|
||||
@@ -22,18 +34,18 @@ class Package extends BasePackage
|
||||
|
||||
$this->settings = $package->toArray();
|
||||
|
||||
$html_description = \Parsedown::instance()->line($this->description);
|
||||
$this->data->set('slug', $package->slug);
|
||||
$html_description = Parsedown::instance()->line($this->__get('description'));
|
||||
$this->data->set('slug', $package->__get('slug'));
|
||||
$this->data->set('description_html', $html_description);
|
||||
$this->data->set('description_plain', strip_tags($html_description));
|
||||
$this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->slug));
|
||||
$this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->__get('slug')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->settings['enabled'];
|
||||
return (bool)$this->settings['enabled'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,6 +11,10 @@ namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\GPM\Common\CachedCollection;
|
||||
|
||||
/**
|
||||
* Class Packages
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Packages extends CachedCollection
|
||||
{
|
||||
public function __construct()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,11 +11,13 @@ namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Class Plugins
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $type = 'plugins';
|
||||
|
||||
/**
|
||||
@@ -24,6 +27,7 @@ class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/** @var \Grav\Common\Plugins $plugins */
|
||||
$plugins = Grav::instance()['plugins'];
|
||||
|
||||
parent::__construct($plugins->all());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,11 +11,13 @@ namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Class Themes
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Themes extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $type = 'themes';
|
||||
|
||||
/**
|
||||
@@ -22,6 +25,9 @@ class Themes extends AbstractPackageCollection
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Grav::instance()['themes']->all());
|
||||
/** @var \Grav\Common\Themes $themes */
|
||||
$themes = Grav::instance()['themes'];
|
||||
|
||||
parent::__construct($themes->all());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -12,36 +13,36 @@ use Grav\Common\Grav;
|
||||
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
|
||||
use Grav\Common\GPM\Response;
|
||||
use \Doctrine\Common\Cache\FilesystemCache;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class AbstractPackageCollection
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class AbstractPackageCollection extends BaseCollection
|
||||
{
|
||||
/**
|
||||
* The cached data previously fetched
|
||||
* @var string
|
||||
*/
|
||||
/** @var string The cached data previously fetched */
|
||||
protected $raw;
|
||||
|
||||
/**
|
||||
* The lifetime to store the entry in seconds
|
||||
* @var integer
|
||||
*/
|
||||
private $lifetime = 86400;
|
||||
|
||||
/** @var string */
|
||||
protected $repository;
|
||||
|
||||
/** @var FilesystemCache */
|
||||
protected $cache;
|
||||
|
||||
/** @var int The lifetime to store the entry in seconds */
|
||||
private $lifetime = 86400;
|
||||
|
||||
/**
|
||||
* AbstractPackageCollection constructor.
|
||||
*
|
||||
* @param null $repository
|
||||
* @param string|null $repository
|
||||
* @param bool $refresh
|
||||
* @param null $callback
|
||||
* @param callable|null $callback
|
||||
*/
|
||||
public function __construct($repository = null, $refresh = false, $callback = null)
|
||||
{
|
||||
parent::__construct();
|
||||
if ($repository === null) {
|
||||
throw new \RuntimeException("A repository is required to indicate the origin of the remote collection");
|
||||
throw new RuntimeException('A repository is required to indicate the origin of the remote collection');
|
||||
}
|
||||
|
||||
$channel = Grav::instance()['config']->get('system.gpm.releases', 'stable');
|
||||
@@ -53,7 +54,7 @@ class AbstractPackageCollection extends BaseCollection
|
||||
|
||||
$this->fetch($refresh, $callback);
|
||||
foreach (json_decode($this->raw, true) as $slug => $data) {
|
||||
// Temporarily fix for using multisites
|
||||
// Temporarily fix for using multi-sites
|
||||
if (isset($data['install_path'])) {
|
||||
$path = preg_replace('~^user/~i', 'user://', $data['install_path']);
|
||||
$data['install_path'] = Grav::instance()['locator']->findResource($path, false, true);
|
||||
@@ -62,6 +63,11 @@ class AbstractPackageCollection extends BaseCollection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $refresh
|
||||
* @param callable|null $callback
|
||||
* @return string
|
||||
*/
|
||||
public function fetch($refresh = false, $callback = null)
|
||||
{
|
||||
if (!$this->raw || $refresh) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,20 +11,30 @@ namespace Grav\Common\GPM\Remote;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use \Doctrine\Common\Cache\FilesystemCache;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class GravCore
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class GravCore extends AbstractPackageCollection
|
||||
{
|
||||
/** @var string */
|
||||
protected $repository = 'https://getgrav.org/downloads/grav.json';
|
||||
private $data;
|
||||
|
||||
/** @var array */
|
||||
private $data;
|
||||
/** @var string */
|
||||
private $version;
|
||||
/** @var string */
|
||||
private $date;
|
||||
/** @var string|null */
|
||||
private $min_php;
|
||||
|
||||
/**
|
||||
* @param bool $refresh
|
||||
* @param null $callback
|
||||
* @throws \InvalidArgumentException
|
||||
* @param callable|null $callback
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
@@ -36,9 +47,9 @@ class GravCore extends AbstractPackageCollection
|
||||
$this->fetch($refresh, $callback);
|
||||
|
||||
$this->data = json_decode($this->raw, true);
|
||||
$this->version = isset($this->data['version']) ? $this->data['version'] : '-';
|
||||
$this->date = isset($this->data['date']) ? $this->data['date'] : '-';
|
||||
$this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null;
|
||||
$this->version = $this->data['version'] ?? '-';
|
||||
$this->date = $this->data['date'] ?? '-';
|
||||
$this->min_php = $this->data['min_php'] ?? null;
|
||||
|
||||
if (isset($this->data['assets'])) {
|
||||
foreach ((array)$this->data['assets'] as $slug => $data) {
|
||||
@@ -60,8 +71,7 @@ class GravCore extends AbstractPackageCollection
|
||||
/**
|
||||
* Returns the changelog list for each version of Grav
|
||||
*
|
||||
* @param string $diff the version number to start the diff from
|
||||
*
|
||||
* @param string|null $diff the version number to start the diff from
|
||||
* @return array changelog list for each version
|
||||
*/
|
||||
public function getChangelog($diff = null)
|
||||
@@ -72,7 +82,7 @@ class GravCore extends AbstractPackageCollection
|
||||
|
||||
$diffLog = [];
|
||||
foreach ((array)$this->data['changelog'] as $version => $changelog) {
|
||||
preg_match("/[\w-\.]+/", $version, $cleanVersion);
|
||||
preg_match("/[\w\-\.]+/", $version, $cleanVersion);
|
||||
|
||||
if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) {
|
||||
continue;
|
||||
@@ -117,14 +127,15 @@ class GravCore extends AbstractPackageCollection
|
||||
/**
|
||||
* Returns the minimum PHP version
|
||||
*
|
||||
* @return null|string
|
||||
* @return string
|
||||
*/
|
||||
public function getMinPHPVersion()
|
||||
{
|
||||
// If non min set, assume current PHP version
|
||||
if (is_null($this->min_php)) {
|
||||
$this->min_php = phpversion();
|
||||
if (null === $this->min_php) {
|
||||
$this->min_php = PHP_VERSION;
|
||||
}
|
||||
|
||||
return $this->min_php;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -11,9 +12,54 @@ namespace Grav\Common\GPM\Remote;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\GPM\Common\Package as BasePackage;
|
||||
|
||||
class Package extends BasePackage {
|
||||
public function __construct($package, $package_type = null) {
|
||||
/**
|
||||
* Class Package
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Package extends BasePackage implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Package constructor.
|
||||
* @param array $package
|
||||
* @param string|null $package_type
|
||||
*/
|
||||
public function __construct($package, $package_type = null)
|
||||
{
|
||||
$data = new Data($package);
|
||||
parent::__construct($data, $package_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->data->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the changelog list for each version of a package
|
||||
*
|
||||
* @param string|null $diff the version number to start the diff from
|
||||
* @return array changelog list for each version
|
||||
*/
|
||||
public function getChangelog($diff = null)
|
||||
{
|
||||
if (!$diff) {
|
||||
return $this->data['changelog'];
|
||||
}
|
||||
|
||||
$diffLog = [];
|
||||
foreach ((array)$this->data['changelog'] as $version => $changelog) {
|
||||
preg_match("/[\w\-.]+/", $version, $cleanVersion);
|
||||
|
||||
if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diffLog[$version] = $changelog;
|
||||
}
|
||||
|
||||
return $diffLog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,8 +11,17 @@ namespace Grav\Common\GPM\Remote;
|
||||
|
||||
use Grav\Common\GPM\Common\CachedCollection;
|
||||
|
||||
/**
|
||||
* Class Packages
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Packages extends CachedCollection
|
||||
{
|
||||
/**
|
||||
* Packages constructor.
|
||||
* @param bool $refresh
|
||||
* @param callable|null $callback
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$items = [
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
/**
|
||||
* Class Plugins
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $type = 'plugins';
|
||||
|
||||
/** @var string */
|
||||
protected $repository = 'https://getgrav.org/downloads/plugins.json';
|
||||
|
||||
/**
|
||||
* Local Plugins Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
* @param callable|null $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
/**
|
||||
* Class Themes
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Themes extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $type = 'themes';
|
||||
|
||||
/** @var string */
|
||||
protected $repository = 'https://getgrav.org/downloads/themes.json';
|
||||
|
||||
/**
|
||||
* Local Themes Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
* @param callable|null $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
|
||||
@@ -1,208 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Grav;
|
||||
use Symfony\Component\HttpClient\CurlHttpClient;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpClient\NativeHttpClient;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use function call_user_func;
|
||||
use function defined;
|
||||
use function function_exists;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* The callback for the progress
|
||||
*
|
||||
* @var callable Either a function or callback in array notation
|
||||
*/
|
||||
/** @var callable The callback for the progress, either a function or callback in array notation */
|
||||
public static $callback = null;
|
||||
|
||||
/**
|
||||
* Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $method = 'auto';
|
||||
|
||||
/**
|
||||
* Default parameters for `curl` and `fopen`
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $defaults = [
|
||||
|
||||
'curl' => [
|
||||
CURLOPT_REFERER => 'Grav GPM',
|
||||
CURLOPT_USERAGENT => 'Grav GPM',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HEADER => false,
|
||||
//CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
|
||||
/**
|
||||
* Example of callback parameters from within your own class
|
||||
*/
|
||||
//CURLOPT_NOPROGRESS => false,
|
||||
//CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
|
||||
],
|
||||
'fopen' => [
|
||||
'method' => 'GET',
|
||||
'user_agent' => 'Grav GPM',
|
||||
'max_redirects' => 5,
|
||||
'follow_location' => 1,
|
||||
'timeout' => 15,
|
||||
/* // this is set in the constructor since it's a setting
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
*/
|
||||
/**
|
||||
* Example of callback parameters from within your own class
|
||||
*/
|
||||
//'notification' => [$this, 'progress']
|
||||
]
|
||||
/** @var string[] */
|
||||
private static $headers = [
|
||||
'User-Agent' => 'Grav CMS'
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the preferred method to use for making HTTP calls.
|
||||
*
|
||||
* @param string $method Default is `auto`
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public static function setMethod($method = 'auto')
|
||||
{
|
||||
if (!in_array($method, ['auto', 'curl', 'fopen'])) {
|
||||
$method = 'auto';
|
||||
}
|
||||
|
||||
self::$method = $method;
|
||||
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the URL by using the preferred method
|
||||
*
|
||||
* @param string $uri URL to call
|
||||
* @param array $options An array of parameters for both `curl` and `fopen`
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*
|
||||
* @param string $uri URL to call
|
||||
* @param array $overrides An array of parameters for both `curl` and `fopen`
|
||||
* @param callable|null $callback Either a function or callback in array notation
|
||||
* @return string The response of the request
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public static function get($uri = '', $options = [], $callback = null)
|
||||
public static function get($uri = '', $overrides = [], $callback = null)
|
||||
{
|
||||
if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
|
||||
throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
|
||||
if (empty($uri)) {
|
||||
throw new TransportException('missing URI');
|
||||
}
|
||||
|
||||
// check if this function is available, if so use it to stop any timeouts
|
||||
try {
|
||||
if (!Utils::isFunctionDisabled('set_time_limit') && !ini_get('safe_mode') && function_exists('set_time_limit')) {
|
||||
set_time_limit(0);
|
||||
if (Utils::functionExists('set_time_limit')) {
|
||||
@set_time_limit(0);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
$config = Grav::instance()['config'];
|
||||
$overrides = [];
|
||||
$referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
|
||||
$options = new HttpOptions();
|
||||
|
||||
// Override CA Bundle
|
||||
$caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
|
||||
if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
|
||||
$overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile;
|
||||
$overrides['fopen']['ssl']['capath'] = $caPathOrFile;
|
||||
} else {
|
||||
$overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile;
|
||||
$overrides['fopen']['ssl']['cafile'] = $caPathOrFile;
|
||||
// Set default Headers
|
||||
$options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers));
|
||||
|
||||
// Disable verify Peer if required
|
||||
$verify_peer = $config->get('system.gpm.verify_peer', true);
|
||||
if ($verify_peer !== true) {
|
||||
$options->verifyPeer($verify_peer);
|
||||
}
|
||||
|
||||
// SSL Verify Peer and Proxy Setting
|
||||
$settings = [
|
||||
'method' => $config->get('system.gpm.method', self::$method),
|
||||
'verify_peer' => $config->get('system.gpm.verify_peer', true),
|
||||
// `system.proxy_url` is for fallback
|
||||
// introduced with 1.1.0-beta.1 probably safe to remove at some point
|
||||
'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
|
||||
];
|
||||
|
||||
if (!$settings['verify_peer']) {
|
||||
$overrides = array_replace_recursive([], $overrides, [
|
||||
'curl' => [
|
||||
CURLOPT_SSL_VERIFYPEER => $settings['verify_peer']
|
||||
],
|
||||
'fopen' => [
|
||||
'ssl' => [
|
||||
'verify_peer' => $settings['verify_peer'],
|
||||
'verify_peer_name' => $settings['verify_peer'],
|
||||
]
|
||||
]
|
||||
]);
|
||||
// Set proxy url if provided
|
||||
$proxy_url = $config->get('system.gpm.proxy_url', false);
|
||||
if ($proxy_url) {
|
||||
$options->setProxy($proxy_url);
|
||||
}
|
||||
|
||||
// Proxy Setting
|
||||
if ($settings['proxy_url']) {
|
||||
$proxy = parse_url($settings['proxy_url']);
|
||||
$fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
|
||||
|
||||
$overrides = array_replace_recursive([], $overrides, [
|
||||
'curl' => [
|
||||
CURLOPT_PROXY => $proxy['host'],
|
||||
CURLOPT_PROXYTYPE => 'HTTP'
|
||||
],
|
||||
'fopen' => [
|
||||
'proxy' => $fopen_proxy,
|
||||
'request_fulluri' => true
|
||||
]
|
||||
]);
|
||||
|
||||
if (isset($proxy['port'])) {
|
||||
$overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
|
||||
}
|
||||
|
||||
if (isset($proxy['user']) && isset($proxy['pass'])) {
|
||||
$fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
|
||||
$overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
|
||||
$overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
|
||||
}
|
||||
// Use callback if provided
|
||||
if ($callback) {
|
||||
self::$callback = $callback;
|
||||
$options->setOnProgress([Response::class, 'progress']);
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(self::$defaults, $options, $overrides);
|
||||
$method = 'get' . ucfirst(strtolower($settings['method']));
|
||||
$preferred_method = $config->get('system.gpm.method', 'auto');
|
||||
|
||||
self::$callback = $callback;
|
||||
return static::$method($uri, $options, $callback);
|
||||
$settings = array_merge_recursive($options->toArray(), $overrides);
|
||||
|
||||
switch ($preferred_method) {
|
||||
case 'curl':
|
||||
$client = new CurlHttpClient($settings);
|
||||
break;
|
||||
case 'fopen':
|
||||
case 'native':
|
||||
$client = new NativeHttpClient($settings);
|
||||
break;
|
||||
default:
|
||||
$client = HttpClient::create($settings);
|
||||
}
|
||||
|
||||
$response = $client->request('GET', $uri);
|
||||
|
||||
return $response->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cURL is available
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isCurlAvailable()
|
||||
{
|
||||
return function_exists('curl_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the remote fopen request is enabled in PHP
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isFopenAvailable()
|
||||
{
|
||||
return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a remote file or not
|
||||
*
|
||||
* @param $file
|
||||
* @param string $file
|
||||
* @return bool
|
||||
*/
|
||||
public static function isRemote($file)
|
||||
@@ -213,220 +119,25 @@ class Response
|
||||
/**
|
||||
* Progress normalized for cURL and Fopen
|
||||
* Accepts a variable length of arguments passed in by stream method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function progress()
|
||||
public static function progress(int $bytes_transferred, int $filesize, array $info)
|
||||
{
|
||||
static $filesize = null;
|
||||
|
||||
$args = func_get_args();
|
||||
$isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) == 'curl';
|
||||
|
||||
$notification_code = !$isCurlResource ? $args[0] : false;
|
||||
$bytes_transferred = $isCurlResource ? $args[2] : $args[4];
|
||||
|
||||
if ($isCurlResource) {
|
||||
$filesize = $args[1];
|
||||
} elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
|
||||
$filesize = $args[5];
|
||||
}
|
||||
|
||||
if ($bytes_transferred > 0) {
|
||||
if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) {
|
||||
$percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize);
|
||||
|
||||
$progress = [
|
||||
'code' => $notification_code,
|
||||
'filesize' => $filesize,
|
||||
'transferred' => $bytes_transferred,
|
||||
'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1)
|
||||
];
|
||||
$progress = [
|
||||
'code' => $info['http_code'],
|
||||
'filesize' => $filesize,
|
||||
'transferred' => $bytes_transferred,
|
||||
'percent' => $percent < 100 ? $percent : 100
|
||||
];
|
||||
|
||||
if (self::$callback !== null) {
|
||||
call_user_func_array(self::$callback, [$progress]);
|
||||
}
|
||||
if (self::$callback !== null) {
|
||||
call_user_func(self::$callback, $progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically picks the preferred method
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getAuto()
|
||||
{
|
||||
if (!ini_get('open_basedir') && self::isFopenAvailable()) {
|
||||
return self::getFopen(func_get_args());
|
||||
}
|
||||
|
||||
if (self::isCurlAvailable()) {
|
||||
return self::getCurl(func_get_args());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a HTTP request via fopen
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getFopen()
|
||||
{
|
||||
if (count($args = func_get_args()) == 1) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
$uri = $args[0];
|
||||
$options = $args[1];
|
||||
$callback = $args[2];
|
||||
|
||||
if ($callback) {
|
||||
$options['fopen']['notification'] = ['self', 'progress'];
|
||||
}
|
||||
|
||||
if (isset($options['fopen']['ssl'])) {
|
||||
$ssl = $options['fopen']['ssl'];
|
||||
unset($options['fopen']['ssl']);
|
||||
|
||||
$stream = stream_context_create([
|
||||
'http' => $options['fopen'],
|
||||
'ssl' => $ssl
|
||||
], $options['fopen']);
|
||||
} else {
|
||||
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
|
||||
}
|
||||
|
||||
|
||||
$content = @file_get_contents($uri, false, $stream);
|
||||
|
||||
if ($content === false) {
|
||||
$code = null;
|
||||
if (isset($http_response_header)) {
|
||||
$code = explode(' ', $http_response_header[0])[1];
|
||||
}
|
||||
|
||||
switch ($code) {
|
||||
case '404':
|
||||
throw new \RuntimeException("Page not found");
|
||||
case '401':
|
||||
throw new \RuntimeException("Invalid LICENSE");
|
||||
default:
|
||||
throw new \RuntimeException("Error while trying to download (code: $code): $uri \n");
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a HTTP request via cURL
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getCurl()
|
||||
{
|
||||
$args = func_get_args();
|
||||
$args = count($args) > 1 ? $args : array_shift($args);
|
||||
|
||||
$uri = $args[0];
|
||||
$options = $args[1];
|
||||
$callback = $args[2];
|
||||
|
||||
$ch = curl_init($uri);
|
||||
|
||||
$response = static::curlExecFollow($ch, $options, $callback);
|
||||
$errno = curl_errno($ch);
|
||||
|
||||
if ($errno) {
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error_message = curl_strerror($errno) . "\n" . curl_error($ch);
|
||||
|
||||
switch ($code) {
|
||||
case '404':
|
||||
throw new \RuntimeException("Page not found");
|
||||
case '401':
|
||||
throw new \RuntimeException("Invalid LICENSE");
|
||||
default:
|
||||
throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ch
|
||||
* @param $options
|
||||
* @param $callback
|
||||
*
|
||||
* @return bool|mixed
|
||||
*/
|
||||
private static function curlExecFollow($ch, $options, $callback)
|
||||
{
|
||||
if ($callback) {
|
||||
curl_setopt_array(
|
||||
$ch,
|
||||
[
|
||||
CURLOPT_NOPROGRESS => false,
|
||||
CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// no open_basedir set, we can proceed normally
|
||||
if (!ini_get('open_basedir')) {
|
||||
curl_setopt_array($ch, $options['curl']);
|
||||
return curl_exec($ch);
|
||||
}
|
||||
|
||||
$max_redirects = isset($options['curl'][CURLOPT_MAXREDIRS]) ? $options['curl'][CURLOPT_MAXREDIRS] : 5;
|
||||
$options['curl'][CURLOPT_FOLLOWLOCATION] = false;
|
||||
|
||||
// open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
|
||||
curl_setopt_array($ch, $options['curl']);
|
||||
if ($max_redirects <= 0) {
|
||||
return curl_exec($ch);
|
||||
}
|
||||
|
||||
$uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
||||
$rch = curl_copy_handle($ch);
|
||||
|
||||
curl_setopt($rch, CURLOPT_HEADER, true);
|
||||
curl_setopt($rch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
|
||||
curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
do {
|
||||
curl_setopt($rch, CURLOPT_URL, $uri);
|
||||
$header = curl_exec($rch);
|
||||
|
||||
if (curl_errno($rch)) {
|
||||
$code = 0;
|
||||
} else {
|
||||
$code = curl_getinfo($rch, CURLINFO_HTTP_CODE);
|
||||
if ($code == 301 || $code == 302 || $code == 303) {
|
||||
preg_match('/Location:(.*?)\n/', $header, $matches);
|
||||
$uri = trim(array_pop($matches));
|
||||
} else {
|
||||
$code = 0;
|
||||
}
|
||||
}
|
||||
} while ($code && --$max_redirects);
|
||||
|
||||
curl_close($rch);
|
||||
|
||||
if (!$max_redirects) {
|
||||
if ($max_redirects === null) {
|
||||
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $uri);
|
||||
|
||||
return curl_exec($ch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.GPM
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\GPM\Remote\GravCore;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class Upgrader
|
||||
@@ -17,21 +19,18 @@ use Grav\Common\GPM\Remote\GravCore;
|
||||
*/
|
||||
class Upgrader
|
||||
{
|
||||
/**
|
||||
* Remote details about latest Grav version
|
||||
*
|
||||
* @var GravCore
|
||||
*/
|
||||
/** @var GravCore Remote details about latest Grav version */
|
||||
private $remote;
|
||||
|
||||
/** @var string|null */
|
||||
private $min_php;
|
||||
|
||||
/**
|
||||
* Creates a new GPM instance with Local and Remote packages available
|
||||
*
|
||||
* @param boolean $refresh Applies to Remote Packages only and forces a refetch of data
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
* @throws \InvalidArgumentException
|
||||
* @param callable|null $callback Either a function or callback in array notation
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
@@ -81,8 +80,7 @@ class Upgrader
|
||||
/**
|
||||
* Returns the changelog list for each version of Grav
|
||||
*
|
||||
* @param string $diff the version number to start the diff from
|
||||
*
|
||||
* @param string|null $diff the version number to start the diff from
|
||||
* @return array return the changelog list for each version
|
||||
*/
|
||||
public function getChangelog($diff = null)
|
||||
@@ -97,8 +95,7 @@ class Upgrader
|
||||
*/
|
||||
public function meetsRequirements()
|
||||
{
|
||||
$current_php_version = phpversion();
|
||||
if (version_compare($current_php_version, $this->minPHPVersion(), '<')) {
|
||||
if (version_compare(PHP_VERSION, $this->minPHPVersion(), '<')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -108,32 +105,32 @@ class Upgrader
|
||||
/**
|
||||
* Get minimum PHP version from remote
|
||||
*
|
||||
* @return null
|
||||
* @return string
|
||||
*/
|
||||
public function minPHPVersion()
|
||||
{
|
||||
if (is_null($this->min_php)) {
|
||||
if (null === $this->min_php) {
|
||||
$this->min_php = $this->remote->getMinPHPVersion();
|
||||
}
|
||||
|
||||
return $this->min_php;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the currently installed Grav is upgradable to a newer version
|
||||
*
|
||||
* @return boolean True if it's upgradable, False otherwise.
|
||||
* @return bool True if it's upgradable, False otherwise.
|
||||
*/
|
||||
public function isUpgradable()
|
||||
{
|
||||
return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<");
|
||||
return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), '<');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Grav is currently symbolically linked
|
||||
*
|
||||
* @return boolean True if Grav is symlinked, False otherwise.
|
||||
* @return bool True if Grav is symlinked, False otherwise.
|
||||
*/
|
||||
|
||||
public function isSymlink()
|
||||
{
|
||||
return $this->remote->isSymlink();
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
abstract class Getters implements \ArrayAccess, \Countable
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Class Getters
|
||||
* @package Grav\Common
|
||||
*/
|
||||
abstract class Getters implements ArrayAccess, Countable
|
||||
{
|
||||
/**
|
||||
* Define variable used in getters.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
/** @var string Define variable used in getters. */
|
||||
protected $gettersVariable = null;
|
||||
|
||||
/**
|
||||
* Magic setter method
|
||||
*
|
||||
* @param mixed $offset Medium name value
|
||||
* @param int|string $offset Medium name value
|
||||
* @param mixed $value Medium value
|
||||
*/
|
||||
public function __set($offset, $value)
|
||||
@@ -31,8 +36,7 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
/**
|
||||
* Magic getter method
|
||||
*
|
||||
* @param mixed $offset Medium name value
|
||||
*
|
||||
* @param int|string $offset Medium name value
|
||||
* @return mixed Medium value
|
||||
*/
|
||||
public function __get($offset)
|
||||
@@ -43,8 +47,7 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
/**
|
||||
* Magic method to determine if the attribute is set
|
||||
*
|
||||
* @param mixed $offset Medium name value
|
||||
*
|
||||
* @param int|string $offset Medium name value
|
||||
* @return boolean True if the value is set
|
||||
*/
|
||||
public function __isset($offset)
|
||||
@@ -55,7 +58,7 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
/**
|
||||
* Magic method to unset the attribute
|
||||
*
|
||||
* @param mixed $offset The name value to unset
|
||||
* @param int|string $offset The name value to unset
|
||||
*/
|
||||
public function __unset($offset)
|
||||
{
|
||||
@@ -63,8 +66,7 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @param int|string $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
@@ -73,14 +75,13 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
$var = $this->gettersVariable;
|
||||
|
||||
return isset($this->{$var}[$offset]);
|
||||
} else {
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @param int|string $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
@@ -88,14 +89,14 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
if ($this->gettersVariable) {
|
||||
$var = $this->gettersVariable;
|
||||
|
||||
return isset($this->{$var}[$offset]) ? $this->{$var}[$offset] : null;
|
||||
} else {
|
||||
return isset($this->{$offset}) ? $this->{$offset} : null;
|
||||
return $this->{$var}[$offset] ?? null;
|
||||
}
|
||||
|
||||
return $this->{$offset} ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param int|string $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
@@ -109,7 +110,7 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param int|string $offset
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
@@ -128,10 +129,10 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
{
|
||||
if ($this->gettersVariable) {
|
||||
$var = $this->gettersVariable;
|
||||
count($this->{$var});
|
||||
} else {
|
||||
count($this->toArray());
|
||||
return count($this->{$var});
|
||||
}
|
||||
|
||||
return count($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,16 +146,16 @@ abstract class Getters implements \ArrayAccess, \Countable
|
||||
$var = $this->gettersVariable;
|
||||
|
||||
return $this->{$var};
|
||||
} else {
|
||||
$properties = (array)$this;
|
||||
$list = [];
|
||||
foreach ($properties as $property => $value) {
|
||||
if ($property[0] != "\0") {
|
||||
$list[$property] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
$properties = (array)$this;
|
||||
$list = [];
|
||||
foreach ($properties as $property => $value) {
|
||||
if ($property[0] !== "\0") {
|
||||
$list[$property] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Config\Setup;
|
||||
use Grav\Common\Helpers\Exif;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Medium\ImageMedium;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Page;
|
||||
use RocketTheme\Toolbox\DI\Container;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Processors\AssetsProcessor;
|
||||
use Grav\Common\Processors\BackupsProcessor;
|
||||
use Grav\Common\Processors\DebuggerAssetsProcessor;
|
||||
use Grav\Common\Processors\InitializeProcessor;
|
||||
use Grav\Common\Processors\PagesProcessor;
|
||||
use Grav\Common\Processors\PluginsProcessor;
|
||||
use Grav\Common\Processors\RenderProcessor;
|
||||
use Grav\Common\Processors\RequestProcessor;
|
||||
use Grav\Common\Processors\SchedulerProcessor;
|
||||
use Grav\Common\Processors\TasksProcessor;
|
||||
use Grav\Common\Processors\ThemesProcessor;
|
||||
use Grav\Common\Processors\TwigProcessor;
|
||||
use Grav\Common\Scheduler\Scheduler;
|
||||
use Grav\Common\Service\AccountsServiceProvider;
|
||||
use Grav\Common\Service\AssetsServiceProvider;
|
||||
use Grav\Common\Service\BackupsServiceProvider;
|
||||
use Grav\Common\Service\ConfigServiceProvider;
|
||||
use Grav\Common\Service\ErrorServiceProvider;
|
||||
use Grav\Common\Service\FilesystemServiceProvider;
|
||||
use Grav\Common\Service\FlexServiceProvider;
|
||||
use Grav\Common\Service\InflectorServiceProvider;
|
||||
use Grav\Common\Service\LoggerServiceProvider;
|
||||
use Grav\Common\Service\OutputServiceProvider;
|
||||
use Grav\Common\Service\PagesServiceProvider;
|
||||
use Grav\Common\Service\RequestServiceProvider;
|
||||
use Grav\Common\Service\SessionServiceProvider;
|
||||
use Grav\Common\Service\StreamsServiceProvider;
|
||||
use Grav\Common\Service\TaskServiceProvider;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\DI\Container;
|
||||
use Grav\Framework\Psr7\Response;
|
||||
use Grav\Framework\RequestHandler\RequestHandler;
|
||||
use Grav\Framework\Session\Messages;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\Event\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use function array_key_exists;
|
||||
use function call_user_func_array;
|
||||
use function function_exists;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
use function is_callable;
|
||||
use function is_int;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Grav container is the heart of Grav.
|
||||
*
|
||||
* @package Grav\Common
|
||||
*/
|
||||
class Grav extends Container
|
||||
{
|
||||
/**
|
||||
* @var string Processed output for the page.
|
||||
*/
|
||||
/** @var string Processed output for the page. */
|
||||
public $output;
|
||||
|
||||
/**
|
||||
* @var static The singleton instance
|
||||
*/
|
||||
/** @var static The singleton instance */
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
@@ -33,54 +81,44 @@ class Grav extends Container
|
||||
* to the dependency injection container.
|
||||
*/
|
||||
protected static $diMap = [
|
||||
'Grav\Common\Service\LoggerServiceProvider',
|
||||
'Grav\Common\Service\ErrorServiceProvider',
|
||||
'uri' => 'Grav\Common\Uri',
|
||||
'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
|
||||
'cache' => 'Grav\Common\Cache',
|
||||
'Grav\Common\Service\SessionServiceProvider',
|
||||
'plugins' => 'Grav\Common\Plugins',
|
||||
'themes' => 'Grav\Common\Themes',
|
||||
'twig' => 'Grav\Common\Twig\Twig',
|
||||
'taxonomy' => 'Grav\Common\Taxonomy',
|
||||
'language' => 'Grav\Common\Language\Language',
|
||||
'pages' => 'Grav\Common\Page\Pages',
|
||||
'Grav\Common\Service\TaskServiceProvider',
|
||||
'Grav\Common\Service\AssetsServiceProvider',
|
||||
'Grav\Common\Service\PageServiceProvider',
|
||||
'Grav\Common\Service\OutputServiceProvider',
|
||||
'browser' => 'Grav\Common\Browser',
|
||||
'exif' => 'Grav\Common\Helpers\Exif',
|
||||
'Grav\Common\Service\StreamsServiceProvider',
|
||||
'Grav\Common\Service\ConfigServiceProvider',
|
||||
'inflector' => 'Grav\Common\Inflector',
|
||||
'siteSetupProcessor' => 'Grav\Common\Processors\SiteSetupProcessor',
|
||||
'configurationProcessor' => 'Grav\Common\Processors\ConfigurationProcessor',
|
||||
'errorsProcessor' => 'Grav\Common\Processors\ErrorsProcessor',
|
||||
'debuggerInitProcessor' => 'Grav\Common\Processors\DebuggerInitProcessor',
|
||||
'initializeProcessor' => 'Grav\Common\Processors\InitializeProcessor',
|
||||
'pluginsProcessor' => 'Grav\Common\Processors\PluginsProcessor',
|
||||
'themesProcessor' => 'Grav\Common\Processors\ThemesProcessor',
|
||||
'tasksProcessor' => 'Grav\Common\Processors\TasksProcessor',
|
||||
'assetsProcessor' => 'Grav\Common\Processors\AssetsProcessor',
|
||||
'twigProcessor' => 'Grav\Common\Processors\TwigProcessor',
|
||||
'pagesProcessor' => 'Grav\Common\Processors\PagesProcessor',
|
||||
'debuggerAssetsProcessor' => 'Grav\Common\Processors\DebuggerAssetsProcessor',
|
||||
'renderProcessor' => 'Grav\Common\Processors\RenderProcessor',
|
||||
AccountsServiceProvider::class,
|
||||
AssetsServiceProvider::class,
|
||||
BackupsServiceProvider::class,
|
||||
ConfigServiceProvider::class,
|
||||
ErrorServiceProvider::class,
|
||||
FilesystemServiceProvider::class,
|
||||
FlexServiceProvider::class,
|
||||
InflectorServiceProvider::class,
|
||||
LoggerServiceProvider::class,
|
||||
OutputServiceProvider::class,
|
||||
PagesServiceProvider::class,
|
||||
RequestServiceProvider::class,
|
||||
SessionServiceProvider::class,
|
||||
StreamsServiceProvider::class,
|
||||
TaskServiceProvider::class,
|
||||
'browser' => Browser::class,
|
||||
'cache' => Cache::class,
|
||||
'events' => EventDispatcher::class,
|
||||
'exif' => Exif::class,
|
||||
'plugins' => Plugins::class,
|
||||
'scheduler' => Scheduler::class,
|
||||
'taxonomy' => Taxonomy::class,
|
||||
'themes' => Themes::class,
|
||||
'twig' => Twig::class,
|
||||
'uri' => Uri::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array All processors that are processed in $this->process()
|
||||
* @var array All middleware processors that are processed in $this->process()
|
||||
*/
|
||||
protected $processors = [
|
||||
'siteSetupProcessor',
|
||||
'configurationProcessor',
|
||||
'errorsProcessor',
|
||||
'debuggerInitProcessor',
|
||||
protected $middleware = [
|
||||
'initializeProcessor',
|
||||
'pluginsProcessor',
|
||||
'themesProcessor',
|
||||
'requestProcessor',
|
||||
'tasksProcessor',
|
||||
'backupsProcessor',
|
||||
'schedulerProcessor',
|
||||
'assetsProcessor',
|
||||
'twigProcessor',
|
||||
'pagesProcessor',
|
||||
@@ -88,12 +126,18 @@ class Grav extends Container
|
||||
'renderProcessor',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $initialized = [];
|
||||
|
||||
/**
|
||||
* Reset the Grav instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetInstance()
|
||||
{
|
||||
if (self::$instance) {
|
||||
// @phpstan-ignore-next-line
|
||||
self::$instance = null;
|
||||
}
|
||||
}
|
||||
@@ -102,12 +146,11 @@ class Grav extends Container
|
||||
* Return the Grav instance. Create it if it's not already instanced
|
||||
*
|
||||
* @param array $values
|
||||
*
|
||||
* @return Grav
|
||||
*/
|
||||
public static function instance(array $values = [])
|
||||
{
|
||||
if (!self::$instance) {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = static::load($values);
|
||||
} elseif ($values) {
|
||||
$instance = self::$instance;
|
||||
@@ -119,28 +162,349 @@ class Grav extends Container
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Grav version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return GRAV_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSetup(): bool
|
||||
{
|
||||
return isset($this->initialized['setup']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Grav instance using specific environment.
|
||||
*
|
||||
* @param string|null $environment
|
||||
* @return $this
|
||||
*/
|
||||
public function setup(string $environment = null)
|
||||
{
|
||||
if (isset($this->initialized['setup'])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->initialized['setup'] = true;
|
||||
|
||||
// Force environment if passed to the method.
|
||||
if ($environment) {
|
||||
Setup::$environment = $environment;
|
||||
}
|
||||
|
||||
// Initialize setup and streams.
|
||||
$this['setup'];
|
||||
$this['streams'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CLI environment.
|
||||
*
|
||||
* Call after `$grav->setup($environment)`
|
||||
*
|
||||
* - Load configuration
|
||||
* - Initialize logger
|
||||
* - Disable debugger
|
||||
* - Set timezone, locale
|
||||
* - Load plugins (call PluginsLoadedEvent)
|
||||
* - Set Pages and Users type to be used in the site
|
||||
*
|
||||
* This method WILL NOT initialize assets, twig or pages.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function initializeCli()
|
||||
{
|
||||
InitializeProcessor::initializeCli($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
// process all processors (e.g. config, initialize, assets, ..., render)
|
||||
foreach ($this->processors as $processor) {
|
||||
$processor = $this[$processor];
|
||||
$this->measureTime($processor->id, $processor->title, function () use ($processor) {
|
||||
$processor->process();
|
||||
});
|
||||
if (isset($this->initialized['process'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Grav if needed.
|
||||
$this->setup();
|
||||
|
||||
$this->initialized['process'] = true;
|
||||
|
||||
$container = new Container(
|
||||
[
|
||||
'initializeProcessor' => function () {
|
||||
return new InitializeProcessor($this);
|
||||
},
|
||||
'backupsProcessor' => function () {
|
||||
return new BackupsProcessor($this);
|
||||
},
|
||||
'pluginsProcessor' => function () {
|
||||
return new PluginsProcessor($this);
|
||||
},
|
||||
'themesProcessor' => function () {
|
||||
return new ThemesProcessor($this);
|
||||
},
|
||||
'schedulerProcessor' => function () {
|
||||
return new SchedulerProcessor($this);
|
||||
},
|
||||
'requestProcessor' => function () {
|
||||
return new RequestProcessor($this);
|
||||
},
|
||||
'tasksProcessor' => function () {
|
||||
return new TasksProcessor($this);
|
||||
},
|
||||
'assetsProcessor' => function () {
|
||||
return new AssetsProcessor($this);
|
||||
},
|
||||
'twigProcessor' => function () {
|
||||
return new TwigProcessor($this);
|
||||
},
|
||||
'pagesProcessor' => function () {
|
||||
return new PagesProcessor($this);
|
||||
},
|
||||
'debuggerAssetsProcessor' => function () {
|
||||
return new DebuggerAssetsProcessor($this);
|
||||
},
|
||||
'renderProcessor' => function () {
|
||||
return new RenderProcessor($this);
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
$default = static function () {
|
||||
return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
|
||||
};
|
||||
|
||||
$collection = new RequestHandler($this->middleware, $default, $container);
|
||||
|
||||
$response = $collection->handle($this['request']);
|
||||
$body = $response->getBody();
|
||||
|
||||
/** @var Messages $messages */
|
||||
$messages = $this['messages'];
|
||||
|
||||
// Prevent caching if session messages were displayed in the page.
|
||||
$noCache = $messages->isCleared();
|
||||
if ($noCache) {
|
||||
$response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
|
||||
}
|
||||
|
||||
// Handle ETag and If-None-Match headers.
|
||||
if ($response->getHeaderLine('ETag') === '1') {
|
||||
$etag = md5($body);
|
||||
$response = $response->withHeader('ETag', '"' . $etag . '"');
|
||||
|
||||
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
|
||||
if ($noCache === false && $search === $etag) {
|
||||
$response = $response->withStatus(304);
|
||||
$body = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Echo page content.
|
||||
$this->header($response);
|
||||
echo $body;
|
||||
|
||||
$this['debugger']->render();
|
||||
|
||||
// Response object can turn off all shutdown processing. This can be used for example to speed up AJAX responses.
|
||||
// Note that using this feature will also turn off response compression.
|
||||
if ($response->getHeaderLine('Grav-Internal-SkipShutdown') !== '1') {
|
||||
register_shutdown_function([$this, 'shutdown']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates Grav request with a response.
|
||||
*
|
||||
* Please use this method instead of calling `die();` or `exit();`. Note that you need to create a response object.
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
* @return void
|
||||
*/
|
||||
public function close(ResponseInterface $response): void
|
||||
{
|
||||
// Make sure nothing extra gets written to the response.
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Close the session.
|
||||
if (isset($this['session'])) {
|
||||
$this['session']->close();
|
||||
}
|
||||
|
||||
/** @var ServerRequestInterface $request */
|
||||
$request = $this['request'];
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this['debugger'];
|
||||
$debugger->render();
|
||||
$response = $debugger->logRequest($request, $response);
|
||||
|
||||
register_shutdown_function([$this, 'shutdown']);
|
||||
$body = $response->getBody();
|
||||
|
||||
/** @var Messages $messages */
|
||||
$messages = $this['messages'];
|
||||
|
||||
// Prevent caching if session messages were displayed in the page.
|
||||
$noCache = $messages->isCleared();
|
||||
if ($noCache) {
|
||||
$response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
|
||||
}
|
||||
|
||||
// Handle ETag and If-None-Match headers.
|
||||
if ($response->getHeaderLine('ETag') === '1') {
|
||||
$etag = md5($body);
|
||||
$response = $response->withHeader('ETag', '"' . $etag . '"');
|
||||
|
||||
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
|
||||
if ($noCache === false && $search === $etag) {
|
||||
$response = $response->withStatus(304);
|
||||
$body = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Echo page content.
|
||||
$this->header($response);
|
||||
echo $body;
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @return void
|
||||
* @deprecated 1.7 Do not use
|
||||
*/
|
||||
public function exit(ResponseInterface $response): void
|
||||
{
|
||||
$this->close($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates Grav request and redirects browser to another location.
|
||||
*
|
||||
* Please use this method instead of calling `header("Location: {$url}", true, 302); exit();`.
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int|null $code Redirection code (30x)
|
||||
* @return void
|
||||
*/
|
||||
public function redirect($route, $code = null): void
|
||||
{
|
||||
$response = $this->getRedirectResponse($route, $code);
|
||||
|
||||
$this->close($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns redirect response object from Grav.
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int|null $code Redirection code (30x)
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRedirectResponse($route, $code = null): ResponseInterface
|
||||
{
|
||||
/** @var Uri $uri */
|
||||
$uri = $this['uri'];
|
||||
|
||||
// Clean route for redirect
|
||||
$route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
|
||||
|
||||
if ($code < 300 || $code > 399) {
|
||||
$code = null;
|
||||
}
|
||||
|
||||
if (null === $code) {
|
||||
// Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html
|
||||
$regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/';
|
||||
preg_match($regex, $route, $matches);
|
||||
if ($matches) {
|
||||
$route = str_replace($matches[1], '', $matches[0]);
|
||||
$code = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
if ($code === null) {
|
||||
$code = $this['config']->get('system.pages.redirect_default_code', 302);
|
||||
}
|
||||
|
||||
if ($uri::isExternal($route)) {
|
||||
$url = $route;
|
||||
} else {
|
||||
$url = rtrim($uri->rootUrl(), '/') . '/';
|
||||
|
||||
if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
|
||||
$url .= trim($route, '/'); // Remove trailing slash
|
||||
} else {
|
||||
$url .= ltrim($route, '/'); // Support trailing slash default routes
|
||||
}
|
||||
}
|
||||
|
||||
return new Response($code, ['Location' => $url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect browser to another location taking language into account (preferred)
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int $code Redirection code (30x)
|
||||
* @return void
|
||||
*/
|
||||
public function redirectLangSafe($route, $code = null)
|
||||
{
|
||||
if (!$this['uri']->isExternal($route)) {
|
||||
$this->redirect($this['pages']->route($route), $code);
|
||||
} else {
|
||||
$this->redirect($route, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response header.
|
||||
*
|
||||
* @param ResponseInterface|null $response
|
||||
* @return void
|
||||
*/
|
||||
public function header(ResponseInterface $response = null)
|
||||
{
|
||||
if (null === $response) {
|
||||
/** @var PageInterface $page */
|
||||
$page = $this['page'];
|
||||
$response = new Response($page->httpResponseCode(), $page->httpHeaders(), '');
|
||||
}
|
||||
|
||||
header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
|
||||
foreach ($response->getHeaders() as $key => $values) {
|
||||
// Skip internal Grav headers.
|
||||
if (strpos($key, 'Grav-Internal-') === 0) {
|
||||
continue;
|
||||
}
|
||||
foreach ($values as $i => $value) {
|
||||
header($key . ': ' . $value, $i === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system locale based on the language and configuration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLocale()
|
||||
{
|
||||
@@ -154,134 +518,54 @@ class Grav extends Container
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect browser to another location.
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int $code Redirection code (30x)
|
||||
* @param object $event
|
||||
* @return object
|
||||
*/
|
||||
public function redirect($route, $code = null)
|
||||
public function dispatchEvent($event)
|
||||
{
|
||||
/** @var Uri $uri */
|
||||
$uri = $this['uri'];
|
||||
/** @var EventDispatcherInterface $events */
|
||||
$events = $this['events'];
|
||||
$eventName = get_class($event);
|
||||
|
||||
//Check for code in route
|
||||
$regex = '/.*(\[(30[1-7])\])$/';
|
||||
preg_match($regex, $route, $matches);
|
||||
if ($matches) {
|
||||
$route = str_replace($matches[1], '', $matches[0]);
|
||||
$code = $matches[2];
|
||||
}
|
||||
$timestamp = microtime(true);
|
||||
$event = $events->dispatch($event);
|
||||
|
||||
if ($code === null) {
|
||||
$code = $this['config']->get('system.pages.redirect_default_code', 302);
|
||||
}
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this['debugger'];
|
||||
$debugger->addEvent($eventName, $event, $events, $timestamp);
|
||||
|
||||
if (isset($this['session'])) {
|
||||
$this['session']->close();
|
||||
}
|
||||
|
||||
if ($uri->isExternal($route)) {
|
||||
$url = $route;
|
||||
} else {
|
||||
$url = rtrim($uri->rootUrl(), '/') . '/';
|
||||
|
||||
if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
|
||||
$url .= trim($route, '/'); // Remove trailing slash
|
||||
} else {
|
||||
$url .= ltrim($route, '/'); // Support trailing slash default routes
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: {$url}", true, $code);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect browser to another location taking language into account (preferred)
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int $code Redirection code (30x)
|
||||
*/
|
||||
public function redirectLangSafe($route, $code = null)
|
||||
{
|
||||
if (!$this['uri']->isExternal($route)) {
|
||||
$this->redirect($this['pages']->route($route), $code);
|
||||
} else {
|
||||
$this->redirect($route, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response header.
|
||||
*/
|
||||
public function header()
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $this['page'];
|
||||
|
||||
$format = $page->templateFormat();
|
||||
|
||||
header('Content-type: ' . Utils::getMimeByExtension($format, 'text/html'));
|
||||
|
||||
$cache_control = $page->cacheControl();
|
||||
|
||||
// Calculate Expires Headers if set to > 0
|
||||
$expires = $page->expires();
|
||||
|
||||
if ($expires > 0) {
|
||||
$expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
|
||||
if (!$cache_control) {
|
||||
header('Cache-Control: max-age=' . $expires);
|
||||
}
|
||||
header('Expires: ' . $expires_date);
|
||||
}
|
||||
|
||||
// Set cache-control header
|
||||
if ($cache_control) {
|
||||
header('Cache-Control: ' . strtolower($cache_control));
|
||||
}
|
||||
|
||||
// Set the last modified time
|
||||
if ($page->lastModified()) {
|
||||
$last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
|
||||
header('Last-Modified: ' . $last_modified_date);
|
||||
}
|
||||
|
||||
// Calculate a Hash based on the raw file
|
||||
if ($page->eTag()) {
|
||||
header('ETag: "' . md5($page->raw() . $page->modified()).'"');
|
||||
}
|
||||
|
||||
// Set HTTP response code
|
||||
if (isset($this['page']->header()->http_response_code)) {
|
||||
http_response_code($this['page']->header()->http_response_code);
|
||||
}
|
||||
|
||||
// Vary: Accept-Encoding
|
||||
if ($this['config']->get('system.pages.vary_accept_encoding', false)) {
|
||||
header('Vary: Accept-Encoding');
|
||||
}
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires an event with optional parameters.
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param Event $event
|
||||
*
|
||||
* @param Event|null $event
|
||||
* @return Event
|
||||
*/
|
||||
public function fireEvent($eventName, Event $event = null)
|
||||
{
|
||||
/** @var EventDispatcher $events */
|
||||
/** @var EventDispatcherInterface $events */
|
||||
$events = $this['events'];
|
||||
if (null === $event) {
|
||||
$event = new Event();
|
||||
}
|
||||
|
||||
return $events->dispatch($eventName, $event);
|
||||
$timestamp = microtime(true);
|
||||
$events->dispatch($event, $eventName);
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this['debugger'];
|
||||
$debugger->addEvent($eventName, $event, $events, $timestamp);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the final content length for the page and flush the buffer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function shutdown()
|
||||
{
|
||||
@@ -295,35 +579,36 @@ class Grav extends Container
|
||||
$this['session']->close();
|
||||
}
|
||||
|
||||
if ($this['config']->get('system.debugger.shutdown.close_connection', true)) {
|
||||
/** @var Config $config */
|
||||
$config = $this['config'];
|
||||
if ($config->get('system.debugger.shutdown.close_connection', true)) {
|
||||
// Flush the response and close the connection to allow time consuming tasks to be performed without leaving
|
||||
// the connection to the client open. This will make page loads to feel much faster.
|
||||
|
||||
// FastCGI allows us to flush all response data to the client and finish the request.
|
||||
$success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
|
||||
|
||||
if (!$success) {
|
||||
// Unfortunately without FastCGI there is no way to force close the connection.
|
||||
// We need to ask browser to close the connection for us.
|
||||
if ($this['config']->get('system.cache.gzip')) {
|
||||
// Flush gzhandler buffer if gzip setting was enabled.
|
||||
ob_end_flush();
|
||||
|
||||
} else {
|
||||
if ($config->get('system.cache.gzip')) {
|
||||
// Flush gzhandler buffer if gzip setting was enabled to get the size of the compressed output.
|
||||
ob_end_flush();
|
||||
} elseif ($config->get('system.cache.allow_webserver_gzip')) {
|
||||
// Let web server to do the hard work.
|
||||
header('Content-Encoding: identity');
|
||||
} elseif (function_exists('apache_setenv')) {
|
||||
// Without gzip we have no other choice than to prevent server from compressing the output.
|
||||
// This action turns off mod_deflate which would prevent us from closing the connection.
|
||||
if ($this['config']->get('system.cache.allow_webserver_gzip')) {
|
||||
header('Content-Encoding: identity');
|
||||
} else {
|
||||
header('Content-Encoding: none');
|
||||
}
|
||||
|
||||
@apache_setenv('no-gzip', '1');
|
||||
} else {
|
||||
// Fall back to unknown content encoding, it prevents most servers from deflating the content.
|
||||
header('Content-Encoding: none');
|
||||
}
|
||||
|
||||
|
||||
// Get length and close the connection.
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
header("Connection: close");
|
||||
header('Connection: close');
|
||||
|
||||
ob_end_flush();
|
||||
@ob_flush();
|
||||
@@ -337,43 +622,58 @@ class Grav extends Container
|
||||
|
||||
/**
|
||||
* Magic Catch All Function
|
||||
* Used to call closures like measureTime on the instance.
|
||||
*
|
||||
* Used to call closures.
|
||||
*
|
||||
* Source: http://stackoverflow.com/questions/419804/closures-as-class-members
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
$closure = $this->$method;
|
||||
call_user_func_array($closure, $args);
|
||||
$closure = $this->{$method} ?? null;
|
||||
|
||||
return is_callable($closure) ? $closure(...$args) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure how long it takes to do an action.
|
||||
*
|
||||
* @param string $timerId
|
||||
* @param string $timerTitle
|
||||
* @param callable $callback
|
||||
* @return mixed Returns value returned by the callable.
|
||||
*/
|
||||
public function measureTime(string $timerId, string $timerTitle, callable $callback)
|
||||
{
|
||||
$debugger = $this['debugger'];
|
||||
$debugger->startTimer($timerId, $timerTitle);
|
||||
$result = $callback();
|
||||
$debugger->stopTimer($timerId);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and return a Grav instance
|
||||
*
|
||||
* @param array $values
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
protected static function load(array $values)
|
||||
{
|
||||
$container = new static($values);
|
||||
|
||||
$container['grav'] = $container;
|
||||
|
||||
$container['debugger'] = new Debugger();
|
||||
$debugger = $container['debugger'];
|
||||
$container['grav'] = function (Container $container) {
|
||||
user_error('Calling $grav[\'grav\'] or {{ grav.grav }} is deprecated since Grav 1.6, just use $grav or {{ grav }}', E_USER_DEPRECATED);
|
||||
|
||||
// closure that measures time by wrapping a function into startTimer and stopTimer
|
||||
// The debugger can be passed to the closure. Should be more performant
|
||||
// then to get it from the container all time.
|
||||
$container->measureTime = function ($timerId, $timerTitle, $callback) use ($debugger) {
|
||||
$debugger->startTimer($timerId, $timerTitle);
|
||||
$callback();
|
||||
$debugger->stopTimer($timerId);
|
||||
return $container;
|
||||
};
|
||||
|
||||
$container->measureTime('_services', 'Services', function () use ($container) {
|
||||
$container->registerServices($container);
|
||||
});
|
||||
$container->registerServices();
|
||||
|
||||
return $container;
|
||||
}
|
||||
@@ -390,44 +690,20 @@ class Grav extends Container
|
||||
{
|
||||
foreach (self::$diMap as $serviceKey => $serviceClass) {
|
||||
if (is_int($serviceKey)) {
|
||||
$this->registerServiceProvider($serviceClass);
|
||||
$this->register(new $serviceClass);
|
||||
} else {
|
||||
$this->registerService($serviceKey, $serviceClass);
|
||||
$this[$serviceKey] = function ($c) use ($serviceClass) {
|
||||
return new $serviceClass($c);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service provider with the container.
|
||||
*
|
||||
* @param string $serviceClass
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerServiceProvider($serviceClass)
|
||||
{
|
||||
$this->register(new $serviceClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service with the container.
|
||||
*
|
||||
* @param string $serviceKey
|
||||
* @param string $serviceClass
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerService($serviceKey, $serviceClass)
|
||||
{
|
||||
$this[$serviceKey] = function ($c) use ($serviceClass) {
|
||||
return new $serviceClass($c);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This attempts to find media, other files, and download them
|
||||
*
|
||||
* @param $path
|
||||
* @param string $path
|
||||
* @return PageInterface|false
|
||||
*/
|
||||
public function fallbackUrl($path)
|
||||
{
|
||||
@@ -444,7 +720,7 @@ class Grav extends Container
|
||||
$supported_types = $config->get('media.types');
|
||||
|
||||
// Check whitelist first, then ensure extension is a valid media type
|
||||
if (!empty($fallback_types) && !\in_array($uri_extension, $fallback_types, true)) {
|
||||
if (!empty($fallback_types) && !in_array($uri_extension, $fallback_types, true)) {
|
||||
return false;
|
||||
}
|
||||
if (!array_key_exists($uri_extension, $supported_types)) {
|
||||
@@ -453,8 +729,9 @@ class Grav extends Container
|
||||
|
||||
$path_parts = pathinfo($path);
|
||||
|
||||
/** @var Page $page */
|
||||
$page = $this['pages']->dispatch($path_parts['dirname'], true);
|
||||
/** @var Pages $pages */
|
||||
$pages = $this['pages'];
|
||||
$page = $pages->find($path_parts['dirname'], true);
|
||||
|
||||
if ($page) {
|
||||
$media = $page->media()->all();
|
||||
@@ -466,7 +743,7 @@ class Grav extends Container
|
||||
/** @var Medium $medium */
|
||||
$medium = $media[$media_file];
|
||||
foreach ($uri->query(null, true) as $action => $params) {
|
||||
if (in_array($action, ImageMedium::$magic_actions)) {
|
||||
if (in_array($action, ImageMedium::$magic_actions, true)) {
|
||||
call_user_func_array([&$medium, $action], explode(',', $params));
|
||||
}
|
||||
}
|
||||
@@ -486,7 +763,7 @@ class Grav extends Container
|
||||
|
||||
if ($extension) {
|
||||
$download = true;
|
||||
if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) {
|
||||
if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
|
||||
$download = false;
|
||||
}
|
||||
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* @deprecated 1.4 Use Grav::instance() instead
|
||||
* @deprecated 1.4 Use Grav::instance() instead.
|
||||
*/
|
||||
trait GravTrait
|
||||
{
|
||||
/** @var Grav */
|
||||
protected static $grav;
|
||||
|
||||
/**
|
||||
* @return Grav
|
||||
* @deprecated 1.4 Use Grav::instance() instead.
|
||||
*/
|
||||
public static function getGrav()
|
||||
{
|
||||
if (!self::$grav) {
|
||||
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$grav) {
|
||||
self::$grav = Grav::instance();
|
||||
}
|
||||
|
||||
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
|
||||
|
||||
return self::$grav;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Helpers
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
class Base32 {
|
||||
protected static $base32Chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
protected static $base32Lookup = array(
|
||||
use function chr;
|
||||
use function count;
|
||||
use function ord;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Base32
|
||||
* @package Grav\Common\Helpers
|
||||
*/
|
||||
class Base32
|
||||
{
|
||||
/** @var string */
|
||||
protected static $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
/** @var array */
|
||||
protected static $base32Lookup = [
|
||||
0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
|
||||
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
|
||||
@@ -22,27 +34,32 @@ class Base32 {
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* Encode in Base32
|
||||
*
|
||||
* @param $bytes
|
||||
* @param string $bytes
|
||||
* @return string
|
||||
*/
|
||||
public static function encode( $bytes ) {
|
||||
$i = 0; $index = 0; $digit = 0;
|
||||
public static function encode($bytes)
|
||||
{
|
||||
$i = 0;
|
||||
$index = 0;
|
||||
$base32 = '';
|
||||
$bytes_len = strlen($bytes);
|
||||
while( $i < $bytes_len ) {
|
||||
$currByte = ord($bytes{$i});
|
||||
$bytesLen = strlen($bytes);
|
||||
|
||||
while ($i < $bytesLen) {
|
||||
$currByte = ord($bytes[$i]);
|
||||
|
||||
/* Is the current digit going to span a byte boundary? */
|
||||
if( $index > 3 ) {
|
||||
if( ($i + 1) < $bytes_len ) {
|
||||
$nextByte = ord($bytes{$i+1});
|
||||
if ($index > 3) {
|
||||
if (($i + 1) < $bytesLen) {
|
||||
$nextByte = ord($bytes[$i+1]);
|
||||
} else {
|
||||
$nextByte = 0;
|
||||
}
|
||||
|
||||
$digit = $currByte & (0xFF >> $index);
|
||||
$index = ($index + 5) % 8;
|
||||
$digit <<= $index;
|
||||
@@ -51,9 +68,12 @@ class Base32 {
|
||||
} else {
|
||||
$digit = ($currByte >> (8 - ($index + 5))) & 0x1F;
|
||||
$index = ($index + 5) % 8;
|
||||
if( $index === 0 ) $i++;
|
||||
if ($index === 0) {
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
$base32 .= self::$base32Chars{$digit};
|
||||
|
||||
$base32 .= self::$base32Chars[$digit];
|
||||
}
|
||||
return $base32;
|
||||
}
|
||||
@@ -61,30 +81,42 @@ class Base32 {
|
||||
/**
|
||||
* Decode in Base32
|
||||
*
|
||||
* @param $base32
|
||||
* @param string $base32
|
||||
* @return string
|
||||
*/
|
||||
public static function decode( $base32 ) {
|
||||
$bytes = array();
|
||||
$base32_len = strlen($base32);
|
||||
for( $i=$base32_len*5/8-1; $i>=0; --$i ) {
|
||||
public static function decode($base32)
|
||||
{
|
||||
$bytes = [];
|
||||
$base32Len = strlen($base32);
|
||||
$base32LookupLen = count(self::$base32Lookup);
|
||||
|
||||
for ($i = $base32Len * 5 / 8 - 1; $i >= 0; --$i) {
|
||||
$bytes[] = 0;
|
||||
}
|
||||
for( $i = 0, $index = 0, $offset = 0; $i < $base32_len; $i++ ) {
|
||||
$lookup = ord($base32{$i}) - ord('0');
|
||||
|
||||
for ($i = 0, $index = 0, $offset = 0; $i < $base32Len; $i++) {
|
||||
$lookup = ord($base32[$i]) - ord('0');
|
||||
|
||||
/* Skip chars outside the lookup table */
|
||||
if( $lookup < 0 || $lookup >= count(self::$base32Lookup) ) {
|
||||
if ($lookup < 0 || $lookup >= $base32LookupLen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$digit = self::$base32Lookup[$lookup];
|
||||
|
||||
/* If this digit is not in the table, ignore it */
|
||||
if( $digit == 0xFF ) continue;
|
||||
if( $index <= 3 ) {
|
||||
if ($digit === 0xFF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($index <= 3) {
|
||||
$index = ($index + 5) % 8;
|
||||
if( $index == 0) {
|
||||
if ($index === 0) {
|
||||
$bytes[$offset] |= $digit;
|
||||
$offset++;
|
||||
if( $offset >= count($bytes) ) break;
|
||||
if ($offset >= count($bytes)) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$bytes[$offset] |= $digit << (8 - $index);
|
||||
}
|
||||
@@ -92,12 +124,18 @@ class Base32 {
|
||||
$index = ($index + 5) % 8;
|
||||
$bytes[$offset] |= ($digit >> $index);
|
||||
$offset++;
|
||||
if ($offset >= count($bytes) ) break;
|
||||
if ($offset >= count($bytes)) {
|
||||
break;
|
||||
}
|
||||
$bytes[$offset] |= $digit << (8 - $index);
|
||||
}
|
||||
}
|
||||
|
||||
$bites = '';
|
||||
foreach( $bytes as $byte ) $bites .= chr($byte);
|
||||
foreach ($bytes as $byte) {
|
||||
$bites .= chr($byte);
|
||||
}
|
||||
|
||||
return $bites;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Helpers
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Common\Uri;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject;
|
||||
use Grav\Common\Page\Medium\Link;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class Excerpts
|
||||
* @package Grav\Common\Helpers
|
||||
*/
|
||||
class Excerpts
|
||||
{
|
||||
/**
|
||||
* Process Grav image media URL from HTML tag
|
||||
*
|
||||
* @param string $html HTML tag e.g. `<img src="image.jpg" />`
|
||||
* @param Page $page The current page object
|
||||
* @return string Returns final HTML string
|
||||
* @param string $html HTML tag e.g. `<img src="image.jpg" />`
|
||||
* @param PageInterface|null $page Page, defaults to the current page object
|
||||
* @return string Returns final HTML string
|
||||
*/
|
||||
public static function processImageHtml($html, Page $page)
|
||||
public static function processImageHtml($html, PageInterface $page = null)
|
||||
{
|
||||
$excerpt = static::getExcerptFromHtml($html, 'img');
|
||||
|
||||
@@ -35,7 +40,7 @@ class Excerpts
|
||||
$excerpt = static::processLinkExcerpt($excerpt, $page, 'image');
|
||||
|
||||
$excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href'];
|
||||
unset ($excerpt['element']['attributes']['href']);
|
||||
unset($excerpt['element']['attributes']['href']);
|
||||
|
||||
$excerpt = static::processImageExcerpt($excerpt, $page);
|
||||
|
||||
@@ -46,6 +51,26 @@ class Excerpts
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Grav page link URL from HTML tag
|
||||
*
|
||||
* @param string $html HTML tag e.g. `<a href="../foo">Page Link</a>`
|
||||
* @param PageInterface|null $page Page, defaults to the current page object
|
||||
* @return string Returns final HTML string
|
||||
*/
|
||||
public static function processLinkHtml($html, PageInterface $page = null)
|
||||
{
|
||||
$excerpt = static::getExcerptFromHtml($html, 'a');
|
||||
|
||||
$original_href = $excerpt['element']['attributes']['href'];
|
||||
$excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
|
||||
$excerpt['element']['attributes']['data-href'] = $original_href;
|
||||
|
||||
$html = static::getHtmlFromExcerpt($excerpt);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Excerpt array from a chunk of HTML
|
||||
*
|
||||
@@ -55,22 +80,35 @@ class Excerpts
|
||||
*/
|
||||
public static function getExcerptFromHtml($html, $tag)
|
||||
{
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadHTML($html);
|
||||
$images = $doc->getElementsByTagName($tag);
|
||||
$excerpt = null;
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$internalErrors = libxml_use_internal_errors(true);
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
libxml_use_internal_errors($internalErrors);
|
||||
|
||||
foreach ($images as $image) {
|
||||
$elements = $doc->getElementsByTagName($tag);
|
||||
$excerpt = null;
|
||||
$inner = [];
|
||||
|
||||
/** @var DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
$attributes = [];
|
||||
foreach ($image->attributes as $name => $value) {
|
||||
foreach ($element->attributes as $name => $value) {
|
||||
$attributes[$name] = $value->value;
|
||||
}
|
||||
$excerpt = [
|
||||
'element' => [
|
||||
'name' => $image->tagName,
|
||||
'name' => $element->tagName,
|
||||
'attributes' => $attributes
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($element->childNodes as $node) {
|
||||
$inner[] = $doc->saveHTML($node);
|
||||
}
|
||||
|
||||
$excerpt = array_merge_recursive($excerpt, ['element' => ['text' => implode('', $inner)]]);
|
||||
|
||||
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
@@ -79,7 +117,7 @@ class Excerpts
|
||||
/**
|
||||
* Rebuild HTML tag from an excerpt array
|
||||
*
|
||||
* @param $excerpt
|
||||
* @param array $excerpt
|
||||
* @return string
|
||||
*/
|
||||
public static function getHtmlFromExcerpt($excerpt)
|
||||
@@ -98,7 +136,7 @@ class Excerpts
|
||||
|
||||
if (isset($element['text'])) {
|
||||
$html .= '>';
|
||||
$html .= $element['text'];
|
||||
$html .= is_array($element['text']) ? static::getHtmlFromExcerpt(['element' => $element['text']]) : $element['text'];
|
||||
$html .= '</'.$element['name'].'>';
|
||||
} else {
|
||||
$html .= ' />';
|
||||
@@ -110,253 +148,44 @@ class Excerpts
|
||||
/**
|
||||
* Process a Link excerpt
|
||||
*
|
||||
* @param $excerpt
|
||||
* @param Page $page
|
||||
* @param array $excerpt
|
||||
* @param PageInterface|null $page Page, defaults to the current page object
|
||||
* @param string $type
|
||||
* @return mixed
|
||||
*/
|
||||
public static function processLinkExcerpt($excerpt, Page $page, $type = 'link')
|
||||
public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link')
|
||||
{
|
||||
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
|
||||
$excerpts = new ExcerptsObject($page);
|
||||
|
||||
$url_parts = static::parseUrl($url);
|
||||
|
||||
// If there is a query, then parse it and build action calls.
|
||||
if (isset($url_parts['query'])) {
|
||||
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
|
||||
$carry[$parts[0]] = $value;
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
// Valid attributes supported.
|
||||
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
|
||||
|
||||
// Unless told to not process, go through actions.
|
||||
if (array_key_exists('noprocess', $actions)) {
|
||||
unset($actions['noprocess']);
|
||||
} else {
|
||||
// Loop through actions for the image and call them.
|
||||
foreach ($actions as $attrib => $value) {
|
||||
$key = $attrib;
|
||||
|
||||
if (in_array($attrib, $valid_attributes, true)) {
|
||||
// support both class and classes.
|
||||
if ($attrib === 'classes') {
|
||||
$attrib = 'class';
|
||||
}
|
||||
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
|
||||
unset($actions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
// If no query elements left, unset query.
|
||||
if (empty($url_parts['query'])) {
|
||||
unset ($url_parts['query']);
|
||||
}
|
||||
|
||||
// Set path to / if not set.
|
||||
if (empty($url_parts['path'])) {
|
||||
$url_parts['path'] = '';
|
||||
}
|
||||
|
||||
// If scheme isn't http(s)..
|
||||
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
|
||||
// Handle custom streams.
|
||||
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
|
||||
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
|
||||
unset($url_parts['stream'], $url_parts['scheme']);
|
||||
}
|
||||
|
||||
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
// Handle paths and such.
|
||||
$url_parts = Uri::convertUrl($page, $url_parts, $type);
|
||||
|
||||
// Build the URL from the component parts and set it on the element.
|
||||
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
|
||||
|
||||
return $excerpt;
|
||||
return $excerpts->processLinkExcerpt($excerpt, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an image excerpt
|
||||
*
|
||||
* @param array $excerpt
|
||||
* @param Page $page
|
||||
* @return mixed
|
||||
* @param PageInterface|null $page Page, defaults to the current page object
|
||||
* @return array
|
||||
*/
|
||||
public static function processImageExcerpt(array $excerpt, Page $page)
|
||||
public static function processImageExcerpt(array $excerpt, PageInterface $page = null)
|
||||
{
|
||||
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
|
||||
$url_parts = static::parseUrl($url);
|
||||
$excerpts = new ExcerptsObject($page);
|
||||
|
||||
$media = null;
|
||||
$filename = null;
|
||||
|
||||
if (!empty($url_parts['stream'])) {
|
||||
$filename = $url_parts['scheme'] . '://' . (isset($url_parts['path']) ? $url_parts['path'] : '');
|
||||
|
||||
$media = $page->media();
|
||||
|
||||
} else {
|
||||
// File is also local if scheme is http(s) and host matches.
|
||||
$local_file = isset($url_parts['path'])
|
||||
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
|
||||
&& (empty($url_parts['host']) || $url_parts['host'] === Grav::instance()['uri']->host());
|
||||
|
||||
if ($local_file) {
|
||||
$filename = basename($url_parts['path']);
|
||||
$folder = dirname($url_parts['path']);
|
||||
|
||||
// Get the local path to page media if possible.
|
||||
if ($folder === $page->url(false, false, false)) {
|
||||
// Get the media objects for this page.
|
||||
$media = $page->media();
|
||||
} else {
|
||||
// see if this is an external page to this one
|
||||
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
|
||||
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
|
||||
|
||||
/** @var Page $ext_page */
|
||||
$ext_page = Grav::instance()['pages']->dispatch($page_route, true);
|
||||
if ($ext_page) {
|
||||
$media = $ext_page->media();
|
||||
} else {
|
||||
Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a media file that matches the path referenced..
|
||||
if ($media && $filename && isset($media[$filename])) {
|
||||
// Get the medium object.
|
||||
/** @var Medium $medium */
|
||||
$medium = $media[$filename];
|
||||
|
||||
// Process operations
|
||||
$medium = static::processMediaActions($medium, $url_parts);
|
||||
$element_excerpt = $excerpt['element']['attributes'];
|
||||
|
||||
$alt = isset($element_excerpt['alt']) ? $element_excerpt['alt'] : '';
|
||||
$title = isset($element_excerpt['title']) ? $element_excerpt['title'] : '';
|
||||
$class = isset($element_excerpt['class']) ? $element_excerpt['class'] : '';
|
||||
$id = isset($element_excerpt['id']) ? $element_excerpt['id'] : '';
|
||||
|
||||
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
|
||||
|
||||
} else {
|
||||
// Not a current page media file, see if it needs converting to relative.
|
||||
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
return $excerpts->processImageExcerpt($excerpt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process media actions
|
||||
*
|
||||
* @param $medium
|
||||
* @param $url
|
||||
* @return mixed
|
||||
* @param Medium $medium
|
||||
* @param string|array $url
|
||||
* @param PageInterface|null $page Page, defaults to the current page object
|
||||
* @return Medium|Link
|
||||
*/
|
||||
public static function processMediaActions($medium, $url)
|
||||
public static function processMediaActions($medium, $url, PageInterface $page = null)
|
||||
{
|
||||
if (!is_array($url)) {
|
||||
$url_parts = parse_url($url);
|
||||
} else {
|
||||
$url_parts = $url;
|
||||
}
|
||||
$excerpts = new ExcerptsObject($page);
|
||||
|
||||
$actions = [];
|
||||
|
||||
// if there is a query, then parse it and build action calls
|
||||
if (isset($url_parts['query'])) {
|
||||
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = isset($parts[1]) ? $parts[1] : null;
|
||||
$carry[] = ['method' => $parts[0], 'params' => $value];
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
|
||||
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
|
||||
}
|
||||
$defaults = Grav::instance()['config']->get('system.images.defaults');
|
||||
if (is_array($defaults) && count($defaults)) {
|
||||
foreach ($defaults as $method => $params) {
|
||||
$actions[] = [
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// loop through actions for the image and call them
|
||||
foreach ($actions as $action) {
|
||||
$matches = [];
|
||||
|
||||
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
|
||||
$args = [explode(',', $matches[1])];
|
||||
} else {
|
||||
$args = explode(',', $action['params']);
|
||||
}
|
||||
|
||||
$medium = call_user_func_array([$medium, $action['method']], $args);
|
||||
}
|
||||
|
||||
if (isset($url_parts['fragment'])) {
|
||||
$medium->urlHash($url_parts['fragment']);
|
||||
}
|
||||
|
||||
return $medium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of parse_url() which works also with local streams.
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|bool
|
||||
*/
|
||||
protected static function parseUrl($url)
|
||||
{
|
||||
$url_parts = Utils::multibyteParseUrl($url);
|
||||
|
||||
if (isset($url_parts['scheme'])) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
// Special handling for the streams.
|
||||
if ($locator->schemeExists($url_parts['scheme'])) {
|
||||
if (isset($url_parts['host'])) {
|
||||
// Merge host and path into a path.
|
||||
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
|
||||
unset($url_parts['host']);
|
||||
}
|
||||
|
||||
$url_parts['stream'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $url_parts;
|
||||
}
|
||||
|
||||
protected static function resolveStream($url)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
return $locator->isStream($url) ? ($locator->findResource($url, false) ?: $locator->findResource($url, false, true)) : $url;
|
||||
return $excerpts->processMediaActions($medium, $url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav.Common.Helpers
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use SebastianBergmann\GlobalState\RuntimeException;
|
||||
use PHPExif\Reader\Reader;
|
||||
use RuntimeException;
|
||||
use function function_exists;
|
||||
|
||||
/**
|
||||
* Class Exif
|
||||
* @package Grav\Common\Helpers
|
||||
*/
|
||||
class Exif
|
||||
{
|
||||
/** @var Reader */
|
||||
public $reader;
|
||||
|
||||
/**
|
||||
@@ -22,20 +30,19 @@ class Exif
|
||||
public function __construct()
|
||||
{
|
||||
if (Grav::instance()['config']->get('system.media.auto_metadata_exif')) {
|
||||
if (function_exists('exif_read_data') && class_exists('\PHPExif\Reader\Reader')) {
|
||||
$this->reader = \PHPExif\Reader\Reader::factory(\PHPExif\Reader\Reader::TYPE_NATIVE);
|
||||
if (function_exists('exif_read_data') && class_exists(Reader::class)) {
|
||||
$this->reader = Reader::factory(Reader::TYPE_NATIVE);
|
||||
} else {
|
||||
throw new \RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration');
|
||||
throw new RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Reader
|
||||
*/
|
||||
public function getReader()
|
||||
{
|
||||
if ($this->reader) {
|
||||
return $this->reader;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->reader;
|
||||
}
|
||||
}
|
||||
|
||||
165
system/src/Grav/Common/Helpers/LogViewer.php
Normal file
165
system/src/Grav/Common/Helpers/LogViewer.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use DateTime;
|
||||
use function array_slice;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class LogViewer
|
||||
* @package Grav\Common\Helpers
|
||||
*/
|
||||
class LogViewer
|
||||
{
|
||||
/** @var string */
|
||||
protected $pattern = '/\[(?P<date>.*)\] (?P<logger>\w+).(?P<level>\w+): (?P<message>.*[^ ]+) (?P<context>[^ ]+) (?P<extra>[^ ]+)/';
|
||||
|
||||
/**
|
||||
* Get the objects of a tailed file
|
||||
*
|
||||
* @param string $filepath
|
||||
* @param int $lines
|
||||
* @param bool $desc
|
||||
* @return array
|
||||
*/
|
||||
public function objectTail($filepath, $lines = 1, $desc = true)
|
||||
{
|
||||
$data = $this->tail($filepath, $lines);
|
||||
$tailed_log = $data ? explode(PHP_EOL, $data) : [];
|
||||
$line_objects = [];
|
||||
|
||||
foreach ($tailed_log as $line) {
|
||||
$line_objects[] = $this->parse($line);
|
||||
}
|
||||
|
||||
return $desc ? $line_objects : array_reverse($line_objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized way to get just the last few entries of a log file
|
||||
*
|
||||
* @param string $filepath
|
||||
* @param int $lines
|
||||
* @return string|false
|
||||
*/
|
||||
public function tail($filepath, $lines = 1)
|
||||
{
|
||||
|
||||
$f = $filepath ? @fopen($filepath, 'rb') : false;
|
||||
if ($f === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
|
||||
|
||||
fseek($f, -1, SEEK_END);
|
||||
if (fread($f, 1) != "\n") {
|
||||
$lines -= 1;
|
||||
}
|
||||
|
||||
// Start reading
|
||||
$output = '';
|
||||
$chunk = '';
|
||||
// While we would like more
|
||||
while (ftell($f) > 0 && $lines >= 0) {
|
||||
// Figure out how far back we should jump
|
||||
$seek = min(ftell($f), $buffer);
|
||||
// Do the jump (backwards, relative to where we are)
|
||||
fseek($f, -$seek, SEEK_CUR);
|
||||
// Read a chunk and prepend it to our output
|
||||
$output = ($chunk = fread($f, $seek)) . $output;
|
||||
// Jump back to where we started reading
|
||||
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
|
||||
// Decrease our line counter
|
||||
$lines -= substr_count($chunk, "\n");
|
||||
}
|
||||
// While we have too many lines
|
||||
// (Because of buffer size we might have read too many)
|
||||
while ($lines++ < 0) {
|
||||
// Find first newline and remove all text before that
|
||||
$output = substr($output, strpos($output, "\n") + 1);
|
||||
}
|
||||
// Close file and return
|
||||
fclose($f);
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to get level color
|
||||
*
|
||||
* @param string $level
|
||||
* @return string
|
||||
*/
|
||||
public static function levelColor($level)
|
||||
{
|
||||
$colors = [
|
||||
'DEBUG' => 'green',
|
||||
'INFO' => 'cyan',
|
||||
'NOTICE' => 'yellow',
|
||||
'WARNING' => 'yellow',
|
||||
'ERROR' => 'red',
|
||||
'CRITICAL' => 'red',
|
||||
'ALERT' => 'red',
|
||||
'EMERGENCY' => 'magenta'
|
||||
];
|
||||
return $colors[$level] ?? 'white';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a monolog row into array bits
|
||||
*
|
||||
* @param string $line
|
||||
* @return array
|
||||
*/
|
||||
public function parse($line)
|
||||
{
|
||||
if (!is_string($line) || strlen($line) === 0) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match($this->pattern, $line, $data);
|
||||
if (!isset($data['date'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
|
||||
if (is_array($matches) && isset($matches[1])) {
|
||||
$data['message'] = trim($matches[1]);
|
||||
$data['trace'] = trim($matches[2]);
|
||||
}
|
||||
|
||||
return array(
|
||||
'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
|
||||
'logger' => $data['logger'],
|
||||
'level' => $data['level'],
|
||||
'message' => $data['message'],
|
||||
'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
|
||||
'context' => json_decode($data['context'], true),
|
||||
'extra' => json_decode($data['extra'], true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse text of trace into an array of lines
|
||||
*
|
||||
* @param string $trace
|
||||
* @param int $rows
|
||||
* @return array
|
||||
*/
|
||||
public static function parseTrace($trace, $rows = 10)
|
||||
{
|
||||
$lines = array_filter(preg_split('/#\d*/m', $trace));
|
||||
|
||||
return array_slice($lines, 0, $rows);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user