first commi

This commit is contained in:
2021-03-05 09:30:59 +01:00
commit bfa5e61d5e
853 changed files with 120974 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
<?php
/**
* @package Grav\Common
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use Closure;
use Grav\Common\Assets\Pipeline;
use Grav\Common\Assets\Traits\LegacyAssetsTrait;
use Grav\Common\Assets\Traits\TestingAssetsTrait;
use Grav\Common\Config\Config;
use Grav\Framework\Object\PropertyObject;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function call_user_func_array;
use function count;
use function func_get_args;
use function is_array;
/**
* Class Assets
* @package Grav\Common
*/
class Assets extends PropertyObject
{
use TestingAssetsTrait;
use LegacyAssetsTrait;
const CSS = 'css';
const JS = 'js';
const CSS_COLLECTION = 'assets_css';
const JS_COLLECTION = 'assets_js';
const CSS_TYPE = Assets\Css::class;
const JS_TYPE = Assets\Js::class;
const INLINE_CSS_TYPE = Assets\InlineCss::class;
const INLINE_JS_TYPE = Assets\InlineJs::class;
/** @const Regex to match CSS and JavaScript files */
const DEFAULT_REGEX = '/.\.(css|js)$/i';
/** @const Regex to match CSS files */
const CSS_REGEX = '/.\.css$/i';
/** @const Regex to match JavaScript files */
const JS_REGEX = '/.\.js$/i';
/** @var string */
protected $assets_dir;
/** @var string */
protected $assets_url;
/** @var array */
protected $assets_css = [];
/** @var array */
protected $assets_js = [];
// Following variables come from the configuration:
/** @var bool */
protected $css_pipeline;
/** @var bool */
protected $css_pipeline_include_externals;
/** @var bool */
protected $css_pipeline_before_excludes;
/** @var bool */
protected $inlinecss_pipeline_include_externals;
/** @var bool */
protected $inlinecss_pipeline_before_excludes;
/** @var bool */
protected $js_pipeline;
/** @var bool */
protected $js_pipeline_include_externals;
/** @var bool */
protected $js_pipeline_before_excludes;
/** @var bool */
protected $inlinejs_pipeline_include_externals;
/** @var bool */
protected $inlinejs_pipeline_before_excludes;
/** @var array */
protected $pipeline_options = [];
/** @var Closure|string */
protected $fetch_command;
/** @var string */
protected $autoload;
/** @var bool */
protected $enable_asset_timestamp;
/** @var array|null */
protected $collections;
/** @var string */
protected $timestamp;
/** @var array Keeping track for order counts (for sorting) */
protected $order = [];
/**
* Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
*
* @return void
*/
public function init()
{
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
$asset_config = (array)$config->get('system.assets');
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_url = $locator->findResource('asset://', false);
$this->config($asset_config);
// Register any preconfigured collections
foreach ((array) $this->collections as $name => $collection) {
$this->registerCollection($name, (array)$collection);
}
}
/**
* Set up configuration options.
*
* All the class properties except 'js' and 'css' are accepted here.
* Also, an extra option 'autoload' may be passed containing an array of
* assets and/or collections that will be automatically added on startup.
*
* @param array $config Configurable options.
* @return $this
*/
public function config(array $config)
{
foreach ($config as $key => $value) {
if ($this->hasProperty($key)) {
$this->setProperty($key, $value);
} elseif (Utils::startsWith($key, 'css_') || Utils::startsWith($key, 'js_')) {
$this->pipeline_options[$key] = $value;
}
}
// Add timestamp if it's enabled
if ($this->enable_asset_timestamp) {
$this->timestamp = Grav::instance()['cache']->getKey();
}
return $this;
}
/**
* Add an asset or a collection of assets.
*
* It automatically detects the asset type (JavaScript, CSS or collection).
* You may add more than one asset passing an array as argument.
*
* @param string|string[] $asset
* @return $this
*/
public function add($asset)
{
$args = func_get_args();
// More than one asset
if (is_array($asset)) {
foreach ($asset as $a) {
array_shift($args);
$args = array_merge([$a], $args);
call_user_func_array([$this, 'add'], $args);
}
} elseif (isset($this->collections[$asset])) {
array_shift($args);
$args = array_merge([$this->collections[$asset]], $args);
call_user_func_array([$this, 'add'], $args);
} else {
// Get extension
$extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
// JavaScript or CSS
if ($extension !== '') {
$extension = strtolower($extension);
if ($extension === 'css') {
call_user_func_array([$this, 'addCss'], $args);
} elseif ($extension === 'js') {
call_user_func_array([$this, 'addJs'], $args);
}
}
}
return $this;
}
/**
* @param string $collection
* @param string $type
* @param string|string[] $asset
* @param array $options
* @return $this
*/
protected function addType($collection, $type, $asset, $options)
{
if (is_array($asset)) {
foreach ($asset as $a) {
$this->addType($collection, $type, $a, $options);
}
return $this;
}
if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) {
$this->addType($collection, $type, $this->collections[$asset], $options);
return $this;
}
// If pipeline disabled, set to position if provided, else after
if (isset($options['pipeline'])) {
if ($options['pipeline'] === false) {
$exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS : $this::CSS;
$excludes = strtolower($exclude_type . '_pipeline_before_excludes');
if ($this->{$excludes}) {
$default = 'after';
} else {
$default = 'before';
}
$options['position'] = $options['position'] ?? $default;
}
unset($options['pipeline']);
}
// Add timestamp
$options['timestamp'] = $this->timestamp;
// Set order
$group = $options['group'] ?? 'head';
$position = $options['position'] ?? 'pipeline';
$orderKey = "{$type}|{$group}|{$position}";
if (!isset($this->order[$orderKey])) {
$this->order[$orderKey] = 0;
}
$options['order'] = $this->order[$orderKey]++;
// Create asset of correct type
$asset_object = new $type();
// If exists
if ($asset_object->init($asset, $options)) {
$this->$collection[md5($asset)] = $asset_object;
}
return $this;
}
/**
* Add a CSS asset or a collection of assets.
*
* @return $this
*/
public function addCss($asset)
{
return $this->addType($this::CSS_COLLECTION, $this::CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::CSS_TYPE));
}
/**
* Add an Inline CSS asset or a collection of assets.
*
* @return $this
*/
public function addInlineCss($asset)
{
return $this->addType($this::CSS_COLLECTION, $this::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_CSS_TYPE));
}
/**
* Add a JS asset or a collection of assets.
*
* @return $this
*/
public function addJs($asset)
{
return $this->addType($this::JS_COLLECTION, $this::JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_TYPE));
}
/**
* Add an Inline JS asset or a collection of assets.
*
* @return $this
*/
public function addInlineJs($asset)
{
return $this->addType($this::JS_COLLECTION, $this::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_TYPE));
}
/**
* Add/replace collection.
*
* @param string $collectionName
* @param array $assets
* @param bool $overwrite
* @return $this
*/
public function registerCollection($collectionName, array $assets, $overwrite = false)
{
if ($overwrite || !isset($this->collections[$collectionName])) {
$this->collections[$collectionName] = $assets;
}
return $this;
}
/**
* @param array $assets
* @param string $key
* @param string $value
* @param bool $sort
* @return array|false
*/
protected function filterAssets($assets, $key, $value, $sort = false)
{
$results = array_filter($assets, function ($asset) use ($key, $value) {
if ($key === 'position' && $value === 'pipeline') {
$type = $asset->getType();
if ($asset->getRemote() && $this->{strtolower($type) . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline') {
if ($this->{strtolower($type) . '_pipeline_before_excludes'}) {
$asset->setPosition('after');
} else {
$asset->setPosition('before');
}
return false;
}
}
if ($asset[$key] === $value) {
return true;
}
return false;
});
if ($sort && !empty($results)) {
$results = $this->sortAssets($results);
}
return $results;
}
/**
* @param array $assets
* @return array
*/
protected function sortAssets($assets)
{
uasort($assets, static function ($a, $b) {
return $b['priority'] <=> $a['priority'] ?: $a['order'] <=> $b['order'];
});
return $assets;
}
/**
* @param string $type
* @param string $group
* @param array $attributes
* @return string
*/
public function render($type, $group = 'head', $attributes = [])
{
$before_output = '';
$pipeline_output = '';
$after_output = '';
$assets = 'assets_' . $type;
$pipeline_enabled = $type . '_pipeline';
$render_pipeline = 'render' . ucfirst($type);
$group_assets = $this->filterAssets($this->$assets, 'group', $group);
$pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
$before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
$after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
// Pipeline
if ($this->{$pipeline_enabled}) {
$options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
$pipeline = new Pipeline($options);
$pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
} else {
foreach ($pipeline_assets as $asset) {
$pipeline_output .= $asset->render();
}
}
// Before Pipeline
foreach ($before_assets as $asset) {
$before_output .= $asset->render();
}
// After Pipeline
foreach ($after_assets as $asset) {
$after_output .= $asset->render();
}
return $before_output . $pipeline_output . $after_output;
}
/**
* Build the CSS link tags.
*
* @param string $group name of the group
* @param array $attributes
* @return string
*/
public function css($group = 'head', $attributes = [])
{
return $this->render('css', $group, $attributes);
}
/**
* Build the JavaScript script tags.
*
* @param string $group name of the group
* @param array $attributes
* @return string
*/
public function js($group = 'head', $attributes = [])
{
return $this->render('js', $group, $attributes);
}
}
+233
View File
@@ -0,0 +1,233 @@
<?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\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_ROOT . $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;
}
/**
*
* 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_ROOT . $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_ROOT, '', $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
View 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() . ">\n";
}
}
@@ -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";
}
}
@@ -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
View 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() . "></script>\n";
}
}
+279
View File
@@ -0,0 +1,279 @@
<?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 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() . ">\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() . "></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;
}
}
@@ -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 = ROOT_DIR . $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;
}
}
@@ -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);
}
}
@@ -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 = rtrim(ROOT_DIR, '/');
// 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
View 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), '/'));
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
/**
* @package Grav\Common
*
* @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 = [];
/**
* Browser constructor.
*/
public function __construct()
{
try {
$this->useragent = parse_user_agent();
} catch (InvalidArgumentException $e) {
$this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
}
}
/**
* Get the current browser identifier
*
* Currently detected browsers:
*
* Android Browser
* BlackBerry Browser
* Camino
* Kindle / Silk
* Firefox / Iceweasel
* Safari
* Internet Explorer
* IEMobile
* Chrome
* Opera
* Midori
* Vivaldi
* TizenBrowser
* Lynx
* Wget
* Curl
*
* @return string the lowercase browser name
*/
public function getBrowser()
{
return strtolower($this->useragent['browser']);
}
/**
* Get the current platform identifier
*
* Currently detected platforms:
*
* Desktop
* -> Windows
* -> Linux
* -> Macintosh
* -> Chrome OS
* Mobile
* -> Android
* -> iPhone
* -> iPad / iPod Touch
* -> Windows Phone OS
* -> Kindle
* -> Kindle Fire
* -> BlackBerry
* -> Playbook
* -> Tizen
* Console
* -> Nintendo 3DS
* -> New Nintendo 3DS
* -> Nintendo Wii
* -> Nintendo WiiU
* -> PlayStation 3
* -> PlayStation 4
* -> PlayStation Vita
* -> Xbox 360
* -> Xbox One
*
* @return string the lowercase platform name
*/
public function getPlatform()
{
return strtolower($this->useragent['platform']);
}
/**
* Get the current full version identifier
*
* @return string the browser full version identifier
*/
public function getLongVersion()
{
return $this->useragent['version'];
}
/**
* Get the current major version identifier
*
* @return int the browser major version identifier
*/
public function getVersion()
{
$version = explode('.', $this->getLongVersion());
return (int)$version[0];
}
/**
* Determine if the request comes from a human, or from a bot/crawler
*
* @return bool
*/
public function isHuman()
{
$browser = $this->getBrowser();
if (empty($browser)) {
return false;
}
if (preg_match('~(bot|crawl)~i', $browser)) {
return false;
}
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');
}
}
+693
View File
@@ -0,0 +1,693 @@
<?php
/**
* @package Grav\Common
*
* @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
* RedisCache
* MemCache
* MemCacheD
* FileSystem
*/
class Cache extends Getters
{
/** @var string Cache key. */
protected $key;
/** @var int */
protected $lifetime;
/** @var int */
protected $now;
/** @var Config $config */
protected $config;
/** @var DoctrineCache\CacheProvider */
protected $driver;
/** @var CacheInterface */
protected $simpleCache;
/** @var string */
protected $driver_name;
/** @var string */
protected $driver_setting;
/** @var bool */
protected $enabled;
/** @var string */
protected $cache_dir;
protected static $standard_remove = [
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
'cache://validated-',
'cache://images',
'asset://',
];
protected static $standard_remove_no_images = [
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
'cache://validated-',
'asset://',
];
protected static $all_remove = [
'cache://',
'cache://images',
'asset://',
'tmp://'
];
protected static $assets_remove = [
'asset://'
];
protected static $images_remove = [
'cache://images'
];
protected static $cache_remove = [
'cache://'
];
protected static $tmp_remove = [
'tmp://'
];
/**
* Constructor
*
* @param Grav $grav
*/
public function __construct(Grav $grav)
{
$this->init($grav);
}
/**
* 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();
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');
$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') . '-' . $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();
$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 bool|int $enabled
* @return void
*/
public function setEnabled($enabled)
{
$this->enabled = (bool)$enabled;
}
/**
* Returns the current enabled state
*
* @return bool
*/
public function getEnabled()
{
return $this->enabled;
}
/**
* Get cache state
*
* @return string
*/
public function getCacheStatus()
{
return 'Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']';
}
/**
* Automatically picks the cache mechanism to use. If you pick one manually it will use that
* If there is no config option for $driver in the config, or it's set to 'auto', it will
* pick the best option based on which cache extensions are installed.
*
* @return DoctrineCache\CacheProvider The cache driver to use
*/
public function getCacheDriver()
{
$setting = $this->driver_setting;
$driver_name = 'file';
// CLI compatibility requires a non-volatile cache driver
if ($this->config->get('system.cache.cli_compatibility') && (
$setting === 'auto' || $this->isVolatileDriver($setting))) {
$setting = $driver_name;
}
if (!$setting || $setting === 'auto') {
if (extension_loaded('apcu')) {
$driver_name = 'apcu';
} elseif (extension_loaded('wincache')) {
$driver_name = 'wincache';
}
} else {
$driver_name = $setting;
}
$this->driver_name = $driver_name;
switch ($driver_name) {
case 'apc':
case 'apcu':
$driver = new DoctrineCache\ApcuCache();
break;
case 'wincache':
$driver = new DoctrineCache\WinCacheCache();
break;
case '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':
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':
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);
} 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 {
throw new LogicException('Redis PHP extension has not been installed');
}
break;
default:
$driver = new DoctrineCache\FilesystemCache($this->cache_dir);
break;
}
return $driver;
}
/**
* 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 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);
}
return false;
}
/**
* Stores a new cached entry.
*
* @param string $id the id of the cached entry
* @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)
{
if ($this->enabled) {
if ($lifetime === null) {
$lifetime = $this->getLifetime();
}
$this->driver->save($id, $data, $lifetime);
}
}
/**
* Deletes an item in the cache based on the id
*
* @param string $id the id of the cached data entry
* @return bool true if the item was deleted successfully
*/
public function delete($id)
{
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;
}
/**
* Returns a boolean state of whether or not the item exists in the cache based on id key
*
* @param string $id the id of the cached data entry
* @return bool true if the cached items exists
*/
public function contains($id)
{
if ($this->enabled) {
return $this->driver->contains(($id));
}
return false;
}
/**
* Getter method to get the cache key
*
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* Setter method to set key (Advanced)
*
* @param string $key
* @return void
*/
public function setKey($key)
{
$this->key = $key;
$this->driver->setNamespace($this->key);
}
/**
* 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')
{
$locator = Grav::instance()['locator'];
$output = [];
$user_config = USER_DIR . 'config/system.yaml';
switch ($remove) {
case 'all':
$remove_paths = self::$all_remove;
break;
case 'assets-only':
$remove_paths = self::$assets_remove;
break;
case 'images-only':
$remove_paths = self::$images_remove;
break;
case 'cache-only':
$remove_paths = self::$cache_remove;
break;
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;
}
$anything = false;
$files = glob($path . '/*');
if (is_array($files)) {
foreach ($files as $file) {
if (is_link($file)) {
$output[] = '<yellow>Skipping symlink: </yellow>' . $file;
} elseif (is_file($file)) {
if (@unlink($file)) {
$anything = true;
}
} elseif (is_dir($file)) {
if (Folder::delete($file)) {
$anything = true;
}
}
}
}
if ($anything) {
$output[] = '<red>Cleared: </red>' . $path . '/*';
}
} catch (Exception $e) {
// stream not found or another error while deleting files.
$output[] = '<red>ERROR: </red>' . $e->getMessage();
}
}
$output[] = '';
if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) {
touch($user_config);
$output[] = '<red>Touched: </red>' . $user_config;
$output[] = '';
}
// Clear stat cache
@clearstatcache();
// Clear opcache
if (function_exists('opcache_reset')) {
@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)
{
if (!$future) {
return;
}
$interval = (int)($future - $this->now);
if ($interval > 0 && $interval < $this->getLifetime()) {
$this->lifetime = $interval;
}
}
/**
* Retrieve the cache lifetime (in seconds)
*
* @return int
*/
public function getLifetime()
{
if ($this->lifetime === null) {
$this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
}
return $this->lifetime;
}
/**
* Returns the current driver name
*
* @return string
*/
public function getDriverName()
{
return $this->driver_name;
}
/**
* Returns the current driver setting
*
* @return string
*/
public function getDriverSetting()
{
return $this->driver_setting;
}
/**
* is this driver a volatile driver in that it resides in PHP process memory
*
* @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 $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');
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* @package Grav\Common
*
* @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';
/**
* Returns the location of composer.
*
* @return string
*/
public static function getComposerLocation()
{
if (!function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
return self::DEFAULT_PATH;
}
// check for global composer install
$path = trim((string)shell_exec('command -v composer'));
// fall back to grav bundled composer
if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
$path = self::DEFAULT_PATH;
}
return $path;
}
/**
* Return the composer executable file path
*
* @return string
*/
public static function getComposerExecutor()
{
$executor = PHP_BINARY . ' ';
$composer = static::getComposerLocation();
if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
$file = fopen($composer, 'rb');
$firstLine = fgets($file);
fclose($file);
if (!preg_match('/^#!.+php/i', $firstLine)) {
$executor = '';
}
}
return $executor . $composer;
}
}
@@ -0,0 +1,269 @@
<?php
/**
* @package Grav\Common\Config
*
* @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. */
public $version = 1;
/** @var string Filename (base name) of the compiled configuration. */
public $name;
/** @var string|bool Configuration checksum. */
public $checksum;
/** @var int Timestamp of compiled configuration */
public $timestamp = 0;
/** @var string Cache folder to be used. */
protected $cacheFolder;
/** @var array List of files to load. */
protected $files;
/** @var string */
protected $path;
/** @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
*/
public function __construct($cacheFolder, array $files, $path)
{
if (!$cacheFolder) {
throw new BadMethodCallException('Cache folder not defined.');
}
$this->path = $path ? rtrim($path, '\\/') . '/' : '';
$this->cacheFolder = $cacheFolder;
$this->files = $files;
}
/**
* Get filename for the compiled PHP file.
*
* @param string|null $name
* @return $this
*/
public function name($name = null)
{
if (!$this->name) {
$this->name = $name ?: md5(json_encode(array_keys($this->files)));
}
return $this;
}
/**
* Function gets called when cached configuration is saved.
*
* @return void
*/
public function modified()
{
}
/**
* Get timestamp of compiled configuration
*
* @return int Timestamp of compiled configuration
*/
public function timestamp()
{
return $this->timestamp ?: time();
}
/**
* Load the configuration.
*
* @return mixed
*/
public function load()
{
if ($this->object) {
return $this->object;
}
$filename = $this->createFilename();
if (!$this->loadCompiledFile($filename) && $this->loadFiles()) {
$this->saveCompiledFile($filename);
}
return $this->object;
}
/**
* Returns checksum from the configuration files.
*
* You can set $this->checksum = false to disable this check.
*
* @return bool|string
*/
public function 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";
}
/**
* Create configuration object.
*
* @param array $data
* @return void
*/
abstract protected function createObject(array $data = []);
/**
* Finalize configuration object.
*
* @return void
*/
abstract protected function finalizeObject();
/**
* Load single configuration file and append it to the correct position.
*
* @param string $name Name of the position.
* @param string|string[] $filename File(s) to be loaded.
* @return void
*/
abstract protected function loadFile($name, $filename);
/**
* Load and join all configuration files.
*
* @return bool
* @internal
*/
protected function loadFiles()
{
$this->createObject();
$list = array_reverse($this->files);
foreach ($list as $files) {
foreach ($files as $name => $item) {
$this->loadFile($name, $this->path . $item['file']);
}
}
$this->finalizeObject();
return true;
}
/**
* Load compiled file.
*
* @param string $filename
* @return bool
* @internal
*/
protected function loadCompiledFile($filename)
{
if (!file_exists($filename)) {
return false;
}
$cache = include $filename;
if (!is_array($cache)
|| !isset($cache['checksum'], $cache['data'], $cache['@class'])
|| $cache['@class'] !== get_class($this)
) {
return false;
}
// Load real file if cache isn't up to date (or is invalid).
if ($cache['checksum'] !== $this->checksum()) {
return false;
}
$this->createObject($cache['data']);
$this->timestamp = $cache['timestamp'] ?? 0;
$this->finalizeObject();
return true;
}
/**
* Save compiled file.
*
* @param string $filename
* @return void
* @throws RuntimeException
* @internal
*/
protected function saveCompiledFile($filename)
{
$file = PhpFile::instance($filename);
// Attempt to lock the file for writing.
try {
$file->lock(false);
} catch (Exception $e) {
// Another process has locked the file; we will check this in a bit.
}
if ($file->locked() === false) {
// File was already locked by another process.
return;
}
$cache = [
'@class' => get_class($this),
'timestamp' => time(),
'checksum' => $this->checksum(),
'files' => $this->files,
'data' => $this->getState()
];
$file->save($cache);
$file->unlock();
$file->free();
$this->modified();
}
/**
* @return array
*/
protected function getState()
{
return $this->object->toArray();
}
}
@@ -0,0 +1,131 @@
<?php
/**
* @package Grav\Common\Config
*
* @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
{
/**
* CompiledBlueprints constructor.
* @param string $cacheFolder
* @param array $files
* @param string $path
*/
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
$this->version = 2;
}
/**
* Returns checksum from the configuration files.
*
* You can set $this->checksum = false to disable this check.
*
* @return bool|string
*/
public function checksum()
{
if (null === $this->checksum) {
$this->checksum = md5(json_encode($this->files) . json_encode($this->getTypes()) . $this->version);
}
return $this->checksum;
}
/**
* Create configuration object.
*
* @param array $data
*/
protected function createObject(array $data = [])
{
$this->object = (new BlueprintSchema($data))->setTypes($this->getTypes());
}
/**
* Get list of form field types.
*
* @return array
*/
protected function getTypes()
{
return Grav::instance()['plugins']->formFieldTypes ?: [];
}
/**
* Finalize configuration object.
*
* @return void
*/
protected function finalizeObject()
{
}
/**
* Load single configuration file and append it to the correct position.
*
* @param string $name Name of the position.
* @param array $files Files to be loaded.
* @return void
*/
protected function loadFile($name, $files)
{
// Load blueprint file.
$blueprint = new Blueprint($files);
$this->object->embed($name, $blueprint->load()->toArray(), '/', true);
}
/**
* Load and join all configuration files.
*
* @return bool
* @internal
*/
protected function loadFiles()
{
$this->createObject();
// Convert file list into parent list.
$list = [];
/** @var array $files */
foreach ($this->files as $files) {
foreach ($files as $name => $item) {
$list[$name][] = $this->path . $item['file'];
}
}
// Load files.
foreach ($list as $name => $files) {
$this->loadFile($name, $files);
}
$this->finalizeObject();
return true;
}
/**
* @return array
*/
protected function getState()
{
return $this->object->getState();
}
}
@@ -0,0 +1,114 @@
<?php
/**
* @package Grav\Common\Config
*
* @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 callable Blueprints loader. */
protected $callable;
/** @var bool */
protected $withDefaults = false;
/**
* CompiledConfig constructor.
* @param string $cacheFolder
* @param array $files
* @param string $path
*/
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
$this->version = 1;
}
/**
* Set blueprints for the configuration.
*
* @param callable $blueprints
* @return $this
*/
public function setBlueprints(callable $blueprints)
{
$this->callable = $blueprints;
return $this;
}
/**
* @param bool $withDefaults
* @return mixed
*/
public function load($withDefaults = false)
{
$this->withDefaults = $withDefaults;
return parent::load();
}
/**
* Create configuration object.
*
* @param array $data
* @return void
*/
protected function createObject(array $data = [])
{
if ($this->withDefaults && empty($data) && is_callable($this->callable)) {
$blueprints = $this->callable;
$data = $blueprints()->getDefaults();
}
$this->object = new Config($data, $this->callable);
}
/**
* Finalize configuration object.
*
* @return void
*/
protected function finalizeObject()
{
$this->object->checksum($this->checksum());
$this->object->timestamp($this->timestamp());
}
/**
* Function gets called when cached configuration is saved.
*
* @return void
*/
public function modified()
{
$this->object->modified(true);
}
/**
* 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.
* @return void
*/
protected function loadFile($name, $filename)
{
$file = CompiledYamlFile::instance($filename);
$this->object->join($name, $file->content(), '/');
$file->free();
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @package Grav\Common\Config
*
* @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;
/**
* Class CompiledLanguages
* @package Grav\Common\Config
*/
class CompiledLanguages extends CompiledBase
{
/**
* CompiledLanguages constructor.
* @param string $cacheFolder
* @param array $files
* @param string $path
*/
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
$this->version = 1;
}
/**
* Create configuration object.
*
* @param array $data
* @return void
*/
protected function createObject(array $data = [])
{
$this->object = new Languages($data);
}
/**
* Finalize configuration object.
*
* @return void
*/
protected function finalizeObject()
{
$this->object->checksum($this->checksum());
$this->object->timestamp($this->timestamp());
}
/**
* Function gets called when cached configuration is saved.
*
* @return void
*/
public function modified()
{
$this->object->modified(true);
}
/**
* 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.
* @return void
*/
protected function loadFile($name, $filename)
{
$file = CompiledYamlFile::instance($filename);
if (preg_match('|languages\.yaml$|', $filename)) {
$this->object->mergeRecursive((array) $file->content());
} else {
$this->object->mergeRecursive([$name => $file->content()]);
}
$file->free();
}
}
+156
View File
@@ -0,0 +1,156 @@
<?php
/**
* @package Grav\Common\Config
*
* @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\Debugger;
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 */
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()
{
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) {
$this->checksum = $checksum;
}
return $this->checksum;
}
/**
* @param bool|null $modified
* @return bool
*/
public function modified($modified = null)
{
if ($modified !== null) {
$this->modified = $modified;
}
return $this->modified;
}
/**
* @param int|null $timestamp
* @return int
*/
public function timestamp($timestamp = null)
{
if ($timestamp !== null) {
$this->timestamp = $timestamp;
}
return $this->timestamp;
}
/**
* @return $this
*/
public function reload()
{
$grav = Grav::instance();
// Load new configuration.
$config = ConfigServiceProvider::load($grav);
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
if ($config->modified()) {
// Update current configuration.
$this->items = $config->toArray();
$this->checksum($config->checksum());
$this->modified(true);
$debugger->addMessage('Configuration was changed and saved.');
}
return $this;
}
/**
* @return void
*/
public function debug()
{
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage('Environment Name: ' . $this->environment);
if ($this->modified()) {
$debugger->addMessage('Configuration reloaded and cached.');
}
}
/**
* @return void
*/
public function init()
{
$setup = Grav::instance()['setup']->toArray();
foreach ($setup as $key => $value) {
if ($key === 'streams' || !is_array($value)) {
// Optimized as streams and simple values are fully defined in setup.
$this->items[$key] = $value;
} else {
$this->joinDefaults($key, $value);
}
}
// Legacy value - Override the media.upload_limit based on PHP values
$this->items['system']['media']['upload_limit'] = Utils::getUploadLimit();
}
/**
* @return mixed
* @deprecated 1.5 Use Grav::instance()['languages'] instead.
*/
public function getLanguages()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use Grav::instance()[\'languages\'] instead', E_USER_DEPRECATED);
return Grav::instance()['languages'];
}
}
@@ -0,0 +1,273 @@
<?php
/**
* @package Grav\Common\Config
*
* @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 = '';
/**
* @param string $base
* @return $this
*/
public function setBase($base)
{
$this->base = $base ? "{$base}/" : '';
return $this;
}
/**
* Return all locations for all the files with a timestamp.
*
* @param array $paths List of folders to look from.
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
* @param int $levels Maximum number of recursive directories.
* @return array
*/
public function locateFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1)
{
$list = [];
foreach ($paths as $folder) {
$list += $this->detectRecursive($folder, $pattern, $levels);
}
return $list;
}
/**
* Return all locations for all the files with a timestamp.
*
* @param array $paths List of folders to look from.
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
* @param int $levels Maximum number of recursive directories.
* @return array
*/
public function getFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1)
{
$list = [];
foreach ($paths as $folder) {
$path = trim(Folder::getRelativePath($folder), '/');
$files = $this->detectRecursive($folder, $pattern, $levels);
$list += $files[trim($path, '/')];
}
return $list;
}
/**
* Return all paths for all the files with a timestamp.
*
* @param array $paths List of folders to look from.
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
* @param int $levels Maximum number of recursive directories.
* @return array
*/
public function listFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1)
{
$list = [];
foreach ($paths as $folder) {
$list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels));
}
return $list;
}
/**
* Find filename from a list of folders.
*
* Note: Only finds the last override.
*
* @param string $filename
* @param array $folders
* @return array
*/
public function locateFileInFolder($filename, array $folders)
{
$list = [];
foreach ($folders as $folder) {
$list += $this->detectInFolder($folder, $filename);
}
return $list;
}
/**
* Find filename from a list of folders.
*
* @param array $folders
* @param string|null $filename
* @return array
*/
public function locateInFolders(array $folders, $filename = null)
{
$list = [];
foreach ($folders as $folder) {
$path = trim(Folder::getRelativePath($folder), '/');
$list[$path] = $this->detectInFolder($folder, $filename);
}
return $list;
}
/**
* Return all existing locations for a single file with a timestamp.
*
* @param array $paths Filesystem paths to look up from.
* @param string $name Configuration file to be located.
* @param string $ext File extension (optional, defaults to .yaml).
* @return array
*/
public function locateFile(array $paths, $name, $ext = '.yaml')
{
$filename = preg_replace('|[.\/]+|', '/', $name) . $ext;
$list = [];
foreach ($paths as $folder) {
$path = trim(Folder::getRelativePath($folder), '/');
if (is_file("{$folder}/{$filename}")) {
$modified = filemtime("{$folder}/{$filename}");
} else {
$modified = 0;
}
$basename = $this->base . $name;
$list[$path] = [$basename => ['file' => "{$path}/{$filename}", 'modified' => $modified]];
}
return $list;
}
/**
* Detects all directories with a configuration file and returns them with last modification time.
*
* @param string $folder Location to look up from.
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
* @param int $levels Maximum number of recursive directories.
* @return array
* @internal
*/
protected function detectRecursive($folder, $pattern, $levels)
{
$path = trim(Folder::getRelativePath($folder), '/');
if (is_dir($folder)) {
// Find all system and user configuration files.
$options = [
'levels' => $levels,
'compare' => 'Filename',
'pattern' => $pattern,
'filters' => [
'pre-key' => $this->base,
'key' => $pattern,
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
}
],
'key' => 'SubPathname'
];
$list = Folder::all($folder, $options);
ksort($list);
} else {
$list = [];
}
return [$path => $list];
}
/**
* Detects all directories with the lookup file and returns them with last modification time.
*
* @param string $folder Location to look up from.
* @param string|null $lookup Filename to be located (defaults to directory name).
* @return array
* @internal
*/
protected function detectInFolder($folder, $lookup = null)
{
$folder = rtrim($folder, '/');
$path = trim(Folder::getRelativePath($folder), '/');
$base = $path === $folder ? '' : ($path ? substr($folder, 0, -strlen($path)) : $folder . '/');
$list = [];
if (is_dir($folder)) {
$iterator = new DirectoryIterator($folder);
foreach ($iterator as $directory) {
if (!$directory->isDir() || $directory->isDot()) {
continue;
}
$name = $directory->getFilename();
$find = ($lookup ?: $name) . '.yaml';
$filename = "{$path}/{$name}/{$find}";
if (file_exists($base . $filename)) {
$basename = $this->base . $name;
$list[$basename] = ['file' => $filename, 'modified' => filemtime($base . $filename)];
}
}
}
return $list;
}
/**
* Detects all plugins with a configuration file and returns them with last modification time.
*
* @param string $folder Location to look up from.
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
* @param int $levels Maximum number of recursive directories.
* @return array
* @internal
*/
protected function detectAll($folder, $pattern, $levels)
{
$path = trim(Folder::getRelativePath($folder), '/');
if (is_dir($folder)) {
// Find all system and user configuration files.
$options = [
'levels' => $levels,
'compare' => 'Filename',
'pattern' => $pattern,
'filters' => [
'pre-key' => $this->base,
'key' => $pattern,
'value' => function (RecursiveDirectoryIterator $file) use ($path) {
return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()];
}
],
'key' => 'SubPathname'
];
$list = Folder::all($folder, $options);
ksort($list);
} else {
$list = [];
}
return $list;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
/**
* @package Grav\Common\Config
*
* @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\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) {
$this->checksum = $checksum;
}
return $this->checksum;
}
/**
* @param bool|null $modified
* @return bool
*/
public function modified($modified = null)
{
if ($modified !== null) {
$this->modified = $modified;
}
return $this->modified;
}
/**
* @param int|null $timestamp
* @return int
*/
public function timestamp($timestamp = null)
{
if ($timestamp !== null) {
$this->timestamp = $timestamp;
}
return $this->timestamp;
}
/**
* @return void
*/
public function reformat()
{
if (isset($this->items['plugins'])) {
$this->items = array_merge_recursive($this->items, $this->items['plugins']);
unset($this->items['plugins']);
}
}
/**
* @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);
}
}
+410
View File
@@ -0,0 +1,410 @@
<?php
/**
* @package Grav\Common\Config
*
* @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 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 = [
'user' => [
'type' => 'ReadOnlyStream',
'force' => true,
'prefixes' => [
'' => [] // 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.
],
'system' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['system'],
]
],
'asset' => [
'type' => 'Stream',
'prefixes' => [
'' => ['assets'],
]
],
'blueprints' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['environment://blueprints', 'user://blueprints', 'system://blueprints'],
]
],
'config' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['environment://config', 'user://config', 'system://config'],
]
],
'plugins' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://plugins'],
]
],
'plugin' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://plugins'],
]
],
'themes' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://themes'],
]
],
'languages' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['environment://languages', 'user://languages', 'system://languages'],
]
],
'image' => [
'type' => 'Stream',
'prefixes' => [
'' => ['user://images', 'system://images']
]
],
'page' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://pages']
]
],
'user-data' => [
'type' => 'Stream',
'force' => true,
'prefixes' => [
'' => ['user://data']
]
],
'account' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://accounts']
]
],
];
/**
* @param Container|array $container
*/
public function __construct($container)
{
// Configure main streams.
$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 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_ROOT . '/' . $setupFile;
}
if (!is_file($setupFile)) {
echo 'GRAV_SETUP_PATH is defined but does not point to existing setup file.';
exit(1);
}
} else {
$setupFile = GRAV_ROOT . '/setup.php';
if (!is_file($setupFile)) {
$setupFile = GRAV_ROOT . '/' . 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'])) {
$setup['streams']['schemes'] = [];
}
$setup['streams']['schemes'] += $this->streams;
// 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_ROOT. '/' . 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', static::$environment);
$this->def('streams.schemes.environment.prefixes', ['' => [$envPath]]);
}
/**
* @return $this
* @throws RuntimeException
* @throws InvalidArgumentException
*/
public function init()
{
$locator = new UniformResourceLocator(GRAV_ROOT);
$files = [];
$guard = 5;
do {
$check = $files;
$this->initializeLocator($locator);
$files = $locator->findResources('config://streams.yaml');
if ($check === $files) {
break;
}
// Update streams.
foreach (array_reverse($files) as $path) {
$file = CompiledYamlFile::instance($path);
$content = (array)$file->content();
if (!empty($content['schemes'])) {
$this->items['streams']['schemes'] = $content['schemes'] + $this->items['streams']['schemes'];
}
}
} while (--$guard);
if (!$guard) {
throw new RuntimeException('Setup: Configuration reload loop detected!');
}
// Make sure we have valid setup.
$this->check($locator);
return $this;
}
/**
* Initialize resource locator by using the configuration.
*
* @param UniformResourceLocator $locator
* @return void
* @throws BadMethodCallException
*/
public function initializeLocator(UniformResourceLocator $locator)
{
$locator->reset();
$schemes = (array) $this->get('streams.schemes', []);
foreach ($schemes as $scheme => $config) {
if (isset($config['paths'])) {
$locator->addPath($scheme, '', $config['paths']);
}
$override = $config['override'] ?? false;
$force = $config['force'] ?? false;
if (isset($config['prefixes'])) {
foreach ((array)$config['prefixes'] as $prefix => $paths) {
$locator->addPath($scheme, $prefix, $paths, $override, $force);
}
}
}
}
/**
* Get available streams and their types from the configuration.
*
* @return array
*/
public function getStreams()
{
$schemes = [];
foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
$type = $config['type'] ?? 'ReadOnlyStream';
if ($type[0] !== '\\') {
$type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
}
$schemes[$scheme] = $type;
}
return $schemes;
}
/**
* @param UniformResourceLocator $locator
* @return void
* @throws InvalidArgumentException
* @throws BadMethodCallException
* @throws RuntimeException
*/
protected function check(UniformResourceLocator $locator)
{
$streams = $this->items['streams']['schemes'] ?? null;
if (!is_array($streams)) {
throw new InvalidArgumentException('Configuration is missing streams.schemes!');
}
$diff = array_keys(array_diff_key($this->streams, $streams));
if ($diff) {
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' => []]);
$this->initializeLocator($locator);
}
// Create security.yaml if it doesn't exist.
$filename = $locator->findResource('config://security.yaml', true, true);
$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);
}
}
}
+589
View File
@@ -0,0 +1,589 @@
<?php
/**
* @package Grav\Common\Data
*
* @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\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 */
protected $context = 'blueprints://';
/** @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.
*
* @param array $types
* @return $this
*/
public function setTypes(array $types)
{
$this->initInternals();
$this->blueprintSchema->setTypes($types);
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.
*
* Fields without default value are ignored in the list.
*
* @return array
*/
public function getDefaults()
{
$this->initInternals();
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;
}
/**
* Merge two arrays by using blueprints.
*
* @param array $data1
* @param array $data2
* @param string|null $name Optional
* @param string $separator Optional
* @return array
*/
public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
{
$this->initInternals();
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.
*
* @param array $data
* @param string $prefix
* @return array
*/
public function extra(array $data, $prefix = '')
{
$this->initInternals();
return $this->blueprintSchema->extra($data, $prefix);
}
/**
* Validate data against blueprints.
*
* @param array $data
* @param array $options
* @return void
* @throws RuntimeException
*/
public function validate(array $data, array $options = [])
{
$this->initInternals();
$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, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
{
$this->initInternals();
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.
*
* @return BlueprintSchema
*/
public function schema()
{
$this->initInternals();
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 (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 array
*/
protected function loadFile($filename)
{
$file = CompiledYamlFile::instance($filename);
$content = (array)$file->content();
$file->free();
return $content;
}
/**
* @param string|array $path
* @param string|null $context
* @return array
*/
protected function getFiles($path, $context = null)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if (is_string($path) && !$locator->isStream($path)) {
if (is_file($path)) {
return [$path];
}
// Find path overrides.
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)) {
$path .= '.yaml';
}
$paths[] = $path;
} else {
$paths = (array) $path;
}
$files = [];
foreach ($paths as $lookup) {
if (is_string($lookup) && strpos($lookup, '://')) {
$files = array_merge($files, $locator->findResources($lookup));
} else {
$files[] = $lookup;
}
}
return array_values(array_unique($files));
}
/**
* @param array $field
* @param string $property
* @param array $call
* @return void
*/
protected function dynamicData(array &$field, $property, array &$call)
{
$params = $call['params'];
if (is_array($params)) {
$function = array_shift($params);
} else {
$function = $params;
$params = [];
}
[$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([$o, $f], $params);
}
}
// If function returns a value,
if (null !== $data) {
if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
// Combine field and @data-field together.
$field[$property] += $data;
} else {
// Or create/replace field with @data-field.
$field[$property] = $data;
}
}
}
/**
* @param array $field
* @param string $property
* @param array $call
* @return void
*/
protected function dynamicConfig(array &$field, $property, array &$call)
{
$params = $call['params'];
if (is_array($params)) {
$value = array_shift($params);
$params = array_shift($params);
} else {
$value = $params;
$params = [];
}
$default = $field[$property] ?? null;
$config = Grav::instance()['config']->get($value, $default);
if (!empty($field['value_only'])) {
$config = array_combine($config, $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')
{
$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);
}
}
}
}
@@ -0,0 +1,409 @@
<?php
/**
* @package Grav\Common\Data
*
* @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,
'placeholder' => true,
'placeholder_key' => true,
'placeholder_value' => true,
'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
* @param array $options
* @return void
* @throws RuntimeException
*/
public function validate(array $data, array $options = [])
{
try {
$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();
}
if (!empty($messages)) {
throw (new ValidationException())->setMessages($messages);
}
}
/**
* @param array $data
* @param array $toggles
* @return array
*/
public function processForm(array $data, array $toggles = [])
{
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
* @param string $prefix
* @return array
*/
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 => $child) {
$val = $rules[$key] ?? $rules['*'] ?? null;
$rule = is_string($val) ? $this->items[$val] : null;
$checkXss = $xss;
if ($rule) {
// Item has been defined in blueprints.
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($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]);
}
}
return $messages;
}
/**
* @param array $data
* @param array $rules
* @param string $parent
* @param bool $missingValuesAsNull
* @param bool $keepEmptyValues
* @return array|null
*/
protected function filterArray(array $data, array $rules, string $parent, bool $missingValuesAsNull, bool $keepEmptyValues)
{
$results = [];
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.
$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') {
// Skip any extra data.
continue;
}
if ($keepEmptyValues || (null !== $field && (!is_array($field) || !empty($field)))) {
$results[$key] = $field;
}
}
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;
}
/**
* @param array $data
* @param array $fields
* @return array
*/
protected function checkRequired(array $data, array $fields)
{
$messages = [];
foreach ($fields as $name => $field) {
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;
}
if ($field['type'] === 'file' && isset($data['data']['name'][$name])) { //handle case of file input fields required
continue;
}
$value = $field['label'] ?? $field['name'];
$language = Grav::instance()['language'];
$message = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
$messages[$field['name']][] = $message;
}
}
return $messages;
}
/**
* @param array $field
* @param string $property
* @param array $call
* @return void
*/
protected function dynamicConfig(array &$field, $property, array &$call)
{
$value = $call['params'];
$default = $field[$property] ?? null;
$config = Grav::instance()['config']->get($value, $default);
if (null !== $config) {
$field[$property] = $config;
}
}
}
+121
View File
@@ -0,0 +1,121 @@
<?php
/**
* @package Grav\Common\Data
*
* @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 = [];
/**
* @param string|array $search Search path.
*/
public function __construct($search = 'blueprints://')
{
$this->search = $search;
}
/**
* Get blueprint.
*
* @param string $type Blueprint type.
* @return Blueprint
* @throws RuntimeException
*/
public function get($type)
{
if (!isset($this->instances[$type])) {
$blueprint = $this->loadFile($type);
$this->instances[$type] = $blueprint;
}
return $this->instances[$type];
}
/**
* Get all available blueprint types.
*
* @return array List of type=>name
*/
public function types()
{
if ($this->types === null) {
$this->types = [];
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
// Get stream / directory iterator.
if ($locator->isStream($this->search)) {
$iterator = $locator->getIterator($this->search);
} else {
$iterator = new DirectoryIterator($this->search);
}
foreach ($iterator as $file) {
if (!$file->isFile() || '.' . $file->getExtension() !== YAML_EXT) {
continue;
}
$name = $file->getBasename(YAML_EXT);
$this->types[$name] = ucfirst(str_replace('_', ' ', $name));
}
}
return $this->types;
}
/**
* Load blueprint file.
*
* @param string $name Name of the blueprint.
* @return Blueprint
*/
protected function loadFile($name)
{
$blueprint = new Blueprint($name);
if (is_array($this->search) || is_object($this->search)) {
// Page types.
$blueprint->setOverrides($this->search);
$blueprint->setContext('blueprints://pages');
} else {
$blueprint->setContext($this->search);
}
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;
}
}
+342
View File
@@ -0,0 +1,342 @@
<?php
/**
* @package Grav\Common\Data
*
* @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\FileInterface;
use RuntimeException;
use function func_get_args;
use function is_array;
use function is_callable;
use function is_object;
/**
* 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 Blueprint|callable|null */
protected $blueprints;
/** @var FileInterface|null */
protected $storage;
/** @var bool */
private $missingValuesAsNull = false;
/** @var bool */
private $keepEmptyValues = true;
/**
* @param array $items
* @param Blueprint|callable|null $blueprints
*/
public function __construct(array $items = [], $blueprints = null)
{
$this->items = $items;
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;
}
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $data->value('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function value($name, $default = null, $separator = '.')
{
return $this->get($name, $default, $separator);
}
/**
* 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 $separator Separator, defaults to '.'
* @return $this
* @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);
}
if (is_object($value)) {
$value = (array) $value;
} elseif (!is_array($value)) {
throw new RuntimeException('Value ' . $value);
}
$value = $this->blueprints()->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->blueprints()->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 $separator Separator, defaults to '.'
* @return $this
*/
public function joinDefaults($name, $value, $separator = '.')
{
if (is_object($value)) {
$value = (array) $value;
}
$old = $this->get($name, null, $separator);
if ($old !== null) {
$value = $this->blueprints()->mergeData($value, $old, $name, $separator);
}
$this->set($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 = '.')
{
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->blueprints()->mergeData($old, $value, $name, $separator);
}
/**
* Merge two configurations together.
*
* @param array $data
* @return $this
*/
public function merge(array $data)
{
$this->items = $this->blueprints()->mergeData($this->items, $data);
return $this;
}
/**
* Set default values to the configuration if variables were not set.
*
* @param array $data
* @return $this
*/
public function setDefaults(array $data)
{
$this->items = $this->blueprints()->mergeData($data, $this->items);
return $this;
}
/**
* Validate by blueprints.
*
* @return $this
* @throws Exception
*/
public function validate()
{
$this->blueprints()->validate($this->items);
return $this;
}
/**
* @return $this
*/
public function filter()
{
$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;
}
/**
* Get extra items which haven't been defined in blueprints.
*
* @return array
*/
public function extra()
{
return $this->blueprints()->extra($this->items);
}
/**
* Return blueprints.
*
* @return Blueprint
*/
public function blueprints()
{
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.
*
* @return void
* @throws RuntimeException
*/
public function save()
{
$file = $this->file();
if ($file) {
$file->save($this->items);
}
}
/**
* Returns whether the data already exists in the storage.
*
* NOTE: This method does not check if the data is current.
*
* @return bool
*/
public function exists()
{
$file = $this->file();
return $file && $file->exists();
}
/**
* 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 ($storage) {
$this->storage = $storage;
}
return $this->storage;
}
/**
* @return array
*/
public function jsonSerialize()
{
return $this->items;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @package Grav\Common\Data
*
* @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
{
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $data->value('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function value($name, $default = null, $separator = '.');
/**
* Merge external data.
*
* @param array $data
* @return mixed
*/
public function merge(array $data);
/**
* Return blueprints.
*
* @return Blueprint
*/
public function blueprints();
/**
* Validate by blueprints.
*
* @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|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
@@ -0,0 +1,52 @@
<?php
/**
* @package Grav\Common\Data
*
* @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
* @package Grav\Common\Data
*/
class ValidationException extends RuntimeException
{
/** @var array */
protected $messages = [];
/**
* @param array $messages
* @return $this
*/
public function setMessages(array $messages = [])
{
$this->messages = $messages;
$language = Grav::instance()['language'];
$this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
foreach ($messages as $variable => &$list) {
$list = array_unique($list);
foreach ($list as $message) {
$this->message .= "<br/>$message";
}
}
return $this;
}
/**
* @return array
*/
public function getMessages()
{
return $this->messages;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
<?php
/**
* @package Grav\Common\Errors
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Errors;
use Whoops\Handler\Handler;
/**
* Class BareHandler
* @package Grav\Common\Errors
*/
class BareHandler extends Handler
{
/**
* @return int
*/
public function handle()
{
$inspector = $this->getInspector();
$code = $inspector->getException()->getCode();
if (($code >= 400) && ($code < 600)) {
$this->getRun()->sendHttpCode($code);
}
return Handler::QUIT;
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* @package Grav\Common\Errors
*
* @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\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';
// Setup Whoops-based error handler
$system = new SystemFacade;
$whoops = new Run($system);
$verbosity = 1;
if (isset($config['display'])) {
if (is_int($config['display'])) {
$verbosity = $config['display'];
} else {
$verbosity = $config['display'] ? 1 : 0;
}
}
switch ($verbosity) {
case 1:
$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->prependHandler($error_page);
break;
case -1:
$whoops->prependHandler(new BareHandler);
break;
default:
$whoops->prependHandler(new SimplePageHandler);
break;
}
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) {
try {
$logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
} catch (Exception $e) {
echo $e;
}
});
}
$whoops->register();
// Re-register deprecation handler.
$grav['debugger']->setErrorHandler();
}
}
@@ -0,0 +1,52 @@
html, body {
height: 100%
}
body {
margin:0 3rem;
padding:0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1.5rem;
line-height: 1.4;
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
.container {
margin: 0rem;
max-width: 600px;
padding-bottom:5rem;
}
header {
color: #000;
font-size: 4rem;
letter-spacing: 2px;
line-height: 1.1;
margin-bottom: 2rem;
}
p {
font-family: Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif;
color: #666;
}
h5 {
font-weight: normal;
color: #999;
font-size: 1rem;
}
h6 {
font-weight: normal;
color: #999;
}
code {
font-weight: bold;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
@@ -0,0 +1,30 @@
<?php
/**
* Layout template file for Whoops's pretty error output.
*/
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Whoops there was an error!</title>
<style><?php echo $stylesheet ?></style>
</head>
<body>
<div class="container">
<div class="details">
<header>
Server Error
</header>
<p>Sorry, something went terribly wrong!</p>
<h3><?php echo $code ?> - <?php echo $message ?></h3>
<h5>For further details please review your <code>logs/</code> folder, or enable displaying of errors in your system configuration.</h5>
</div>
</div>
</body>
</html>
@@ -0,0 +1,122 @@
<?php
/**
* @package Grav\Common\Errors
*
* @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
{
/** @var array */
private $searchPaths = [];
/** @var array */
private $resourceCache = [];
public function __construct()
{
// Add the default, local resource search path:
$this->searchPaths[] = __DIR__ . '/Resources';
}
/**
* @return int
*/
public function handle()
{
$inspector = $this->getInspector();
$helper = new TemplateHelper();
$templateFile = $this->getResource('layout.html.php');
$cssFile = $this->getResource('error.css');
$code = $inspector->getException()->getCode();
if (($code >= 400) && ($code < 600)) {
$this->getRun()->sendHttpCode($code);
}
$message = $inspector->getException()->getMessage();
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),
);
$helper->setVariables($vars);
$helper->render($templateFile);
return Handler::QUIT;
}
/**
* @param string $resource
* @return string
* @throws RuntimeException
*/
protected function getResource($resource)
{
// If the resource was found before, we can speed things up
// by caching its absolute, resolved path:
if (isset($this->resourceCache[$resource])) {
return $this->resourceCache[$resource];
}
// Search through available search paths, until we find the
// resource we're after:
foreach ($this->searchPaths as $path) {
$fullPath = "{$path}/{$resource}";
if (is_file($fullPath)) {
// Cache the result:
$this->resourceCache[$resource] = $fullPath;
return $fullPath;
}
}
// If we got this far, nothing was found.
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(
"'{$path}' is not a valid directory"
);
}
array_unshift($this->searchPaths, $path);
}
/**
* @return array
*/
public function getResourcePaths()
{
return $this->searchPaths;
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @package Grav\Common\Errors
*
* @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)
{
$this->whoopsShutdownHandler = $function;
register_shutdown_function([$this, 'handleShutdown']);
}
/**
* Special case to deal with Fatal errors and the like.
*
* @return void
*/
public function handleShutdown()
{
$error = $this->getLastError();
// Ignore core warnings and errors.
if ($error && !($error['type'] & (E_CORE_WARNING | E_CORE_ERROR))) {
$handler = $this->whoopsShutdownHandler;
$handler();
}
}
}
@@ -0,0 +1,122 @@
<?php
/**
* @package Grav\Common\File
*
* @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 array
*/
public function content($var = null)
{
try {
// If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($this->filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified();
if (!$modified) {
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);
$cache = $file->exists() ? $file->content() : null;
// Load real file if cache isn't up to date (or is invalid).
if (!isset($cache['@class'])
|| $cache['@class'] !== $class
|| $cache['modified'] !== $modified
|| $cache['filename'] !== $this->filename
) {
// Attempt to lock the file for writing.
try {
$file->lock(false);
} catch (Exception $e) {
// Another process has locked the file; we will check this in a bit.
}
// Decode RAW file into compiled array.
$data = (array)$this->decode($this->raw());
$cache = [
'@class' => $class,
'filename' => $this->filename,
'modified' => $modified,
'data' => $data
];
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$file->save($cache);
$file->unlock();
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) {
// Silence error if function exists, but is restricted.
@opcache_invalidate($file->filename(), true);
}
}
}
$file->free();
$this->content = $cache['data'];
}
} catch (Exception $e) {
throw new RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
}
return parent::content($var);
}
/**
* Serialize file.
*
* @return array
*/
public function __sleep()
{
return [
'filename',
'extension',
'raw',
'content',
'settings'
];
}
/**
* Unserialize file.
*/
public function __wakeup()
{
if (!isset(static::$instances[$this->filename])) {
static::$instances[$this->filename] = $this;
}
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @package Grav\Common\File
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\File;
use RocketTheme\Toolbox\File\JsonFile;
/**
* Class CompiledJsonFile
* @package Grav\Common\File
*/
class CompiledJsonFile extends JsonFile
{
use CompiledFile;
/**
* Decode RAW string into contents.
*
* @param string $var
* @param bool $assoc
* @return array
*/
protected function decode($var, $assoc = true)
{
return (array)json_decode($var, $assoc);
}
}
@@ -0,0 +1,21 @@
<?php
/**
* @package Grav\Common\File
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\File;
use RocketTheme\Toolbox\File\MarkdownFile;
/**
* Class CompiledMarkdownFile
* @package Grav\Common\File
*/
class CompiledMarkdownFile extends MarkdownFile
{
use CompiledFile;
}
@@ -0,0 +1,21 @@
<?php
/**
* @package Grav\Common\File
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\File;
use RocketTheme\Toolbox\File\YamlFile;
/**
* Class CompiledYamlFile
* @package Grav\Common\File
*/
class CompiledYamlFile extends YamlFile
{
use CompiledFile;
}
@@ -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;
}
}
@@ -0,0 +1,540 @@
<?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 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
{
/**
* Recursively find the last modified time under given path.
*
* @param string $path
* @return int
*/
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;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$filter = new RecursiveFolderFilterIterator($directory);
$iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $dir) {
$dir_modified = $dir->getMTime();
if ($dir_modified > $last_modified) {
$last_modified = $dir_modified;
}
}
return $last_modified;
}
/**
* Recursively find the last modified time under given path by file.
*
* @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;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
/** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $filepath => $file) {
try {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;
}
} catch (Exception $e) {
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
}
}
return $last_modified;
}
/**
* Recursively md5 hash all files in a path
*
* @param string $path
* @return string
*/
public static function hashAllFiles($path)
{
$files = [];
if (file_exists($path)) {
$flags = RecursiveDirectoryIterator::SKIP_DOTS;
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
$files[] = $file->getPathname() . '?'. $file->getMTime();
}
}
return md5(serialize($files));
}
/**
* Get relative path between target and base path. If path isn't relative, return full path.
*
* @param string $path
* @param string $base
* @return string
*/
public static function getRelativePath($path, $base = GRAV_ROOT)
{
if ($base) {
$base = preg_replace('![\\\/]+!', '/', $base);
$path = preg_replace('![\\\/]+!', '/', $path);
if (strpos($path, $base) === 0) {
$path = ltrim(substr($path, strlen($base)), '/');
}
}
return $path;
}
/**
* Get relative path between target and base path. If path isn't relative, return full path.
*
* @param string $path
* @param string $base
* @return string
*/
public static function getRelativePathDotDot($path, $base)
{
// Normalize paths.
$base = preg_replace('![\\\/]+!', '/', $base);
$path = preg_replace('![\\\/]+!', '/', $path);
if ($path === $base) {
return '';
}
$baseParts = explode('/', ltrim($base, '/'));
$pathParts = explode('/', ltrim($path, '/'));
array_pop($baseParts);
$lastPart = array_pop($pathParts);
foreach ($baseParts as $i => $directory) {
if (isset($pathParts[$i]) && $pathParts[$i] === $directory) {
unset($baseParts[$i], $pathParts[$i]);
} else {
break;
}
}
$pathParts[] = $lastPart;
$path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
return '' === $path
|| strpos($path, '/') === 0
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
? "./$path" : $path;
}
/**
* Shift first directory out of the path.
*
* @param string $path
* @return string
*/
public static function shift(&$path)
{
$parts = explode('/', trim($path, '/'), 2);
$result = array_shift($parts);
$path = array_shift($parts);
return $result ?: null;
}
/**
* Return recursive list of all files and directories under given path.
*
* @param string $path
* @param array $params
* @return array
* @throws RuntimeException
*/
public static function all($path, array $params = [])
{
if (!$path) {
throw new RuntimeException("Path doesn't exist.");
}
if (!file_exists($path)) {
return [];
}
$compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
$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 = '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;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$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);
}
}
$results = [];
/** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $file) {
// Ignore hidden files.
if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
continue;
}
if (!$folders && $file->isDir()) {
continue;
}
if (!$files && $file->isFile()) {
continue;
}
if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
continue;
}
$fileKey = $key ? $file->{$key}() : null;
$filePath = $file->{$value}();
if ($filters) {
if (isset($filters['key'])) {
$pre = !empty($filters['pre-key']) ? $filters['pre-key'] : '';
$fileKey = $pre . preg_replace($filters['key'], '', $fileKey);
}
if (isset($filters['value'])) {
$filter = $filters['value'];
if (is_callable($filter)) {
$filePath = $filter($file);
} else {
$filePath = preg_replace($filter, '', $filePath);
}
}
}
if ($fileKey !== null) {
$results[$fileKey] = $filePath;
} else {
$results[] = $filePath;
}
}
return $results;
}
/**
* Recursively copy directory in filesystem.
*
* @param string $source
* @param string $target
* @param string|null $ignore Ignore files matching pattern (regular expression).
* @return void
* @throws RuntimeException
*/
public static function copy($source, $target, $ignore = null)
{
$source = rtrim($source, '\\/');
$target = rtrim($target, '\\/');
if (!is_dir($source)) {
throw new RuntimeException('Cannot copy non-existing folder.');
}
// Make sure that path to the target exists before copying.
self::create($target);
$success = true;
// Go through all sub-directories and copy everything.
$files = self::all($source);
foreach ($files as $file) {
if ($ignore && preg_match($ignore, $file)) {
continue;
}
$src = $source .'/'. $file;
$dst = $target .'/'. $file;
if (is_dir($src)) {
// Create current directory (if it doesn't exist).
if (!is_dir($dst)) {
$success &= @mkdir($dst, 0777, true);
}
} else {
// Or copy current file.
$success &= @copy($src, $dst);
}
}
if (!$success) {
$error = error_get_last();
throw new RuntimeException($error['message'] ?? 'Unknown error');
}
// Make sure that the change will be detected when caching.
@touch(dirname($target));
}
/**
* Move directory in filesystem.
*
* @param string $source
* @param string $target
* @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.');
}
// Don't do anything if the source is the same as the new target
if ($source === $target) {
return;
}
if (file_exists($target)) {
// Rename fails if target folder exists.
throw new RuntimeException('Cannot move files to existing folder/file.');
}
// Make sure that path to the target exists before moving.
self::create(dirname($target));
// Silence warnings (chmod failed etc).
@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);
}
// Rename doesn't support moving folders across filesystems. Use copy instead.
self::copy($source, $target);
self::delete($source);
}
// Make sure that the change will be detected when caching.
@touch(dirname($source));
@touch(dirname($target));
@touch($target);
}
/**
* Recursively delete directory from filesystem.
*
* @param string $target
* @param bool $include_target
* @return bool
* @throws RuntimeException
*/
public static function delete($target, $include_target = true)
{
if (!is_dir($target)) {
return false;
}
$success = self::doDelete($target, $include_target);
if (!$success) {
$error = error_get_last();
throw new RuntimeException($error['message']);
}
// Make sure that the change will be detected when caching.
if ($include_target) {
@touch(dirname($target));
} else {
@touch($target);
}
return $success;
}
/**
* @param string $folder
* @return void
* @throws RuntimeException
*/
public static function mkdir($folder)
{
self::create($folder);
}
/**
* @param string $folder
* @return void
* @throws RuntimeException
*/
public static function create($folder)
{
// Silence error for open_basedir; should fail in mkdir instead.
if (@is_dir($folder)) {
return;
}
$success = @mkdir($folder, 0777, true);
if (!$success) {
// 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 string $src
* @param string $dest
* @return bool
* @throws RuntimeException
*/
public static function rcopy($src, $dest)
{
// If the src is not a directory do a simple file copy
if (!is_dir($src)) {
copy($src, $dest);
return true;
}
// If the destination directory does not exist create it
if (!is_dir($dest)) {
static::create($dest);
}
// Open the source directory to read in files
$i = new DirectoryIterator($src);
foreach ($i as $f) {
if ($f->isFile()) {
copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
} else {
if (!$f->isDot() && $f->isDir()) {
static::rcopy($f->getRealPath(), "{$dest}/{$f}");
}
}
}
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
* @return bool
* @internal
*/
protected static function doDelete($folder, $include_target = true)
{
// Special case for symbolic links.
if ($include_target && is_link($folder)) {
return @unlink($folder);
}
// Go through all items in filesystem and recursively remove everything.
$files = array_diff(scandir($folder, SCANDIR_SORT_NONE), array('.', '..'));
foreach ($files as $file) {
$path = "{$folder}/{$file}";
is_dir($path) ? self::doDelete($path) : @unlink($path);
}
return $include_target ? @rmdir($folder) : true;
}
}
@@ -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);
}
}
@@ -0,0 +1,55 @@
<?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 Grav\Common\Grav;
use RecursiveIterator;
use SplFileInfo;
use function in_array;
/**
* Class RecursiveFolderFilterIterator
* @package Grav\Common\Filesystem
*/
class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
{
/** @var array */
protected static $ignore_folders;
/**
* Create a RecursiveFilterIterator from a RecursiveIterator
*
* @param RecursiveIterator $iterator
* @param array $ignore_folders
*/
public function __construct(RecursiveIterator $iterator, $ignore_folders = [])
{
parent::__construct($iterator);
if (empty($ignore_folders)) {
$ignore_folders = Grav::instance()['config']->get('system.pages.ignore_folders');
}
$this::$ignore_folders = $ignore_folders;
}
/**
* 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 $current */
$current = $this->current();
return $current->isDir() && !in_array($current->getFilename(), $this::$ignore_folders, true);
}
}
@@ -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;
}
}
@@ -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\FlexCollectionTrait;
use Grav\Common\Flex\Traits\FlexGravTrait;
/**
* Class FlexCollection
*
* @package Grav\Common\Flex
* @template TKey
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
* @extends \Grav\Framework\Flex\FlexCollection<TKey,T>
*/
abstract class FlexCollection extends \Grav\Framework\Flex\FlexCollection
{
use FlexGravTrait;
use FlexCollectionTrait;
}
+30
View File
@@ -0,0 +1,30 @@
<?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 TKey
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
* @template C of \Grav\Framework\Flex\Interfaces\FlexCollectionInterface
* @extends \Grav\Framework\Flex\FlexIndex<TKey,T,C>
*/
abstract class FlexIndex extends \Grav\Framework\Flex\FlexIndex
{
use FlexGravTrait;
use FlexIndexTrait;
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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';
}
}
@@ -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
{
}
@@ -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<string,GenericObject>
*/
class GenericCollection extends FlexCollection
{
}
@@ -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<string,GenericObject,GenericCollection>
*/
class GenericIndex extends FlexIndex
{
}
@@ -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
{
}
@@ -0,0 +1,801 @@
<?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<string,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 end date is optional
* Dates can be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param string|false $endDate
* @param string|null $field
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
{
$start = Utils::date2timestamp($startDate);
$end = $endDate ? Utils::date2timestamp($endDate) : false;
$entries = [];
foreach ($this as $key => $object) {
if (!$object) {
continue;
}
$date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
if ($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);
}
/**
* 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) : [];
}
}
@@ -0,0 +1,972 @@
<?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\Debugger;
use Grav\Common\File\CompiledJsonFile;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexIndexTrait;
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\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Grav\Framework\Flex\Pages\FlexPageIndex;
use InvalidArgumentException;
use RuntimeException;
use function is_array;
use function is_string;
/**
* Class GravPageObject
* @package Grav\Plugin\FlexObjects\Types\GravPages
*
* @extends FlexPageIndex<string,PageObject,PageCollection>
*
* @method PageIndex withModules(bool $bool = true)
* @method PageIndex withPages(bool $bool = true)
* @method PageIndex withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
*/
class PageIndex extends FlexPageIndex implements PageCollectionInterface
{
use FlexGravTrait;
use FlexIndexTrait;
public const VERSION = parent::VERSION . '.5';
public const ORDER_LIST_REGEX = '/(\/\d+)\.[^\/]+/u';
public const PAGE_ROUTE_REGEX = '/\/\d+\./u';
/** @var PageObject|array */
protected $_root;
/** @var array|null */
protected $_params;
/**
* @param array $entries
* @param FlexDirectory|null $directory
*/
public function __construct(array $entries = [], FlexDirectory $directory = null)
{
// Remove root if it's taken.
if (isset($entries[''])) {
$this->_root = $entries[''];
unset($entries['']);
}
parent::__construct($entries, $directory);
}
/**
* @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, ['include_missing' => true, 'force_update' => $force]);
}
/**
* @param string $key
* @return PageObject|null
*/
public function get($key)
{
if (mb_strpos($key, '|') !== false) {
[$key, $params] = explode('|', $key, 2);
}
$element = parent::get($key);
if (isset($params)) {
$element = $element->getTranslation(ltrim($params, '.'));
}
return $element;
}
/**
* @return PageObject
*/
public function getRoot()
{
$root = $this->_root;
if (is_array($root)) {
$directory = $this->getFlexDirectory();
$storage = $directory->getStorage();
$defaults = [
'header' => [
'routable' => false,
'permissions' => [
'inherit' => false
]
]
];
$row = $storage->readRows(['' => null])[''] ?? null;
if (null !== $row) {
if (isset($row['__ERROR'])) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$message = sprintf('Flex Pages: root page is broken in storage: %s', $row['__ERROR']);
$debugger->addException(new RuntimeException($message));
$debugger->addMessage($message, 'error');
$row = ['__META' => $root];
}
} else {
$row = ['__META' => $root];
}
$row = array_merge_recursive($defaults, $row);
/** @var PageObject $root */
$root = $this->getFlexDirectory()->createObject($row, '/', false);
$root->name('root.md');
$root->root(true);
$this->_root = $root;
}
return $root;
}
/**
* 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();
}
/**
* 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)
{
if (!$filters) {
return $this;
}
if ($recursive) {
return $this->__call('filterBy', [$filters, true]);
}
$list = [];
$index = $this;
foreach ($filters as $key => $value) {
switch ($key) {
case 'search':
$index = $index->search((string)$value);
break;
case 'page_type':
if (!is_array($value)) {
$value = is_string($value) && $value !== '' ? explode(',', $value) : [];
}
$index = $index->ofOneOfTheseTypes($value);
break;
case 'routable':
$index = $index->withRoutable((bool)$value);
break;
case 'published':
$index = $index->withPublished((bool)$value);
break;
case 'visible':
$index = $index->withVisible((bool)$value);
break;
case 'module':
$index = $index->withModules((bool)$value);
break;
case 'page':
$index = $index->withPages((bool)$value);
break;
case 'folder':
$index = $index->withPages(!$value);
break;
case 'translated':
$index = $index->withTranslation((bool)$value);
break;
default:
$list[$key] = $value;
}
}
return $list ? $index->filterByParent($list) : $index;
}
/**
* @param array $filters
* @return static
*/
protected function filterByParent(array $filters)
{
return parent::filterBy($filters);
}
/**
* @param array $options
* @return array
*/
public function getLevelListing(array $options): array
{
// Undocumented B/C
$order = $options['order'] ?? 'asc';
if ($order === SORT_ASC) {
$options['order'] = 'asc';
} elseif ($order === SORT_DESC) {
$options['order'] = 'desc';
}
$options += [
'field' => null,
'route' => null,
'leaf_route' => null,
'sortby' => null,
'order' => 'asc',
'lang' => null,
'filters' => [],
];
$options['filters'] += [
'type' => ['root', 'dir'],
];
return $this->getLevelListingRecurse($options);
}
/**
* @param array $entries
* @param string|null $keyField
* @return static
*/
protected function createFrom(array $entries, string $keyField = null)
{
/** @var static $index */
$index = parent::createFrom($entries, $keyField);
$index->_root = $this->getRoot();
return $index;
}
/**
* @param array $options
* @return array
*/
protected function getLevelListingRecurse(array $options): array
{
$filters = $options['filters'] ?? [];
$field = $options['field'];
$route = $options['route'];
$leaf_route = $options['leaf_route'];
$sortby = $options['sortby'];
$order = $options['order'];
$language = $options['lang'];
$status = 'error';
$msg = null;
$response = [];
$children = null;
$sub_route = null;
$extra = null;
// Handle leaf_route
$leaf = null;
if ($leaf_route && $route !== $leaf_route) {
$nodes = explode('/', $leaf_route);
$sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++));
$options['route'] = $sub_route;
[$status,,$leaf,$extra] = $this->getLevelListingRecurse($options);
}
// Handle no route, assume page tree root
if (!$route) {
$page = $this->getRoot();
} else {
$page = $this->get(trim($route, '/'));
}
$path = $page ? $page->path() : null;
if ($field) {
// Get forced filters from the field.
$blueprint = $page ? $page->getBlueprint() : $this->getFlexDirectory()->getBlueprint();
$settings = $blueprint->schema()->getProperty($field);
$filters = array_merge([], $filters, $settings['filters'] ?? []);
}
// Clean up filter.
$filter_type = (array)($filters['type'] ?? []);
unset($filters['type']);
$filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; });
if ($page) {
if ($page->root() && (!$filter_type || in_array('root', $filter_type, true))) {
if ($field) {
$response[] = [
'name' => '<root>',
'value' => '/',
'item-key' => '',
'filename' => '.',
'extension' => '',
'type' => 'root',
'modified' => $page->modified(),
'size' => 0,
'symlink' => false,
'has-children' => false
];
} else {
$response[] = [
'item-key' => '-root-',
'icon' => 'root',
'title' => 'Root', // FIXME
'route' => [
'display' => '&lt;root&gt;', // FIXME
'raw' => '_root',
],
'modified' => $page->modified(),
'extras' => [
'template' => $page->template(),
//'lang' => null,
//'translated' => null,
'langs' => [],
'published' => false,
'visible' => false,
'routable' => false,
'tags' => ['root', 'non-routable'],
'actions' => ['edit'], // FIXME
]
];
}
}
$status = 'success';
$msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
/** @var PageIndex $children */
$children = $page->children()->getIndex();
$selectedChildren = $children->filterBy($filters, true);
/** @var Header $header */
$header = $page->header();
if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) {
// Use custom sorting by page header.
$sortby = $orderby;
$order = $header->get('content.order.dir', $order);
$custom = $header->get('content.order.custom');
}
if ($sortby) {
// Sort children.
$selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null);
}
/** @var PageObject $child */
foreach ($selectedChildren as $child) {
$selected = $child->path() === $extra;
$includeChildren = is_array($leaf) && !empty($leaf) && $selected;
if ($field) {
$child_count = count($child->children());
$payload = [
'name' => $child->menu(),
'value' => $child->rawRoute(),
'item-key' => basename($child->rawRoute() ?? ''),
'filename' => $child->folder(),
'extension' => $child->extension(),
'type' => 'dir',
'modified' => $child->modified(),
'size' => $child_count,
'symlink' => false,
'has-children' => $child_count > 0
];
} else {
$lang = $child->findTranslation($language) ?? 'n/a';
/** @var PageObject $child */
$child = $child->getTranslation($language) ?? $child;
// TODO: all these features are independent from each other, we cannot just have one icon/color to catch all.
// TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc..
if ($child->home()) {
$icon = 'home';
} elseif ($child->isModule()) {
$icon = 'modular';
} elseif ($child->visible()) {
$icon = 'visible';
} elseif ($child->isPage()) {
$icon = 'page';
} else {
// TODO: add support
$icon = 'folder';
}
$tags = [
$child->published() ? 'published' : 'non-published',
$child->visible() ? 'visible' : 'non-visible',
$child->routable() ? 'routable' : 'non-routable'
];
$extras = [
'template' => $child->template(),
'lang' => $lang ?: null,
'translated' => $lang ? $child->hasTranslation($language, false) : null,
'langs' => $child->getAllLanguages(true) ?: null,
'published' => $child->published(),
'published_date' => $this->jsDate($child->publishDate()),
'unpublished_date' => $this->jsDate($child->unpublishDate()),
'visible' => $child->visible(),
'routable' => $child->routable(),
'tags' => $tags,
'actions' => $this->getListingActions($child),
];
$extras = array_filter($extras, static function ($v) {
return $v !== null;
});
$tmp = $child->children()->getIndex();
$child_count = $tmp->count();
$count = $filters ? $tmp->filterBy($filters, true)->count() : null;
$payload = [
'item-key' => basename($child->rawRoute() ?? $child->getKey()),
'icon' => $icon,
'title' => htmlspecialchars($child->menu()),
'route' => [
'display' => $child->getRoute()->toString(false) ?: '/',
'raw' => $child->rawRoute(),
],
'modified' => $this->jsDate($child->modified()),
'child_count' => $child_count ?: null,
'count' => $count ?? null,
'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null,
'extras' => $extras
];
$payload = array_filter($payload, static function ($v) {
return $v !== null;
});
}
// Add children if any
if ($includeChildren) {
$payload['children'] = array_values($leaf);
}
$response[] = $payload;
}
} else {
$msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
}
if ($field) {
$temp_array = [];
foreach ($response as $index => $item) {
$temp_array[$item['type']][$index] = $item;
}
$sorted = Utils::sortArrayByArray($temp_array, $filter_type);
$response = Utils::arrayFlatten($sorted);
}
return [$status, $msg ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED', $response, $path];
}
/**
* @param PageObject $object
* @return array
*/
protected function getListingActions(PageObject $object): array
{
$actions = [];
if ($object->isAuthorized('read')) {
$actions[] = 'preview';
$actions[] = 'edit';
}
if ($object->isAuthorized('update')) {
$actions[] = 'copy';
$actions[] = 'move';
}
if ($object->isAuthorized('delete')) {
$actions[] = 'delete';
}
return $actions;
}
/**
* @param FlexStorageInterface $storage
* @return CompiledJsonFile|CompiledYamlFile|null
*/
protected static function getIndexFile(FlexStorageInterface $storage)
{
if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) {
return null;
}
// Load saved index file.
$grav = Grav::instance();
$locator = $grav['locator'];
$filename = $locator->findResource('user-data://flex/indexes/pages.json', true, true);
return CompiledJsonFile::instance($filename);
}
/**
* @param int|null $timestamp
* @return string|null
*/
private function jsDate(int $timestamp = null): ?string
{
if (!$timestamp) {
return null;
}
$config = Grav::instance()['config'];
$dateFormat = $config->get('system.pages.dateformat.long');
return date($dateFormat, $timestamp) ?: null;
}
/**
* Add a single page to a collection
*
* @param PageInterface $page
* @return PageCollection
*/
public function addPage(PageInterface $page)
{
return $this->getCollection()->addPage($page);
}
/**
*
* Create a copy of this collection
*
* @return static
*/
public function copy()
{
return clone $this;
}
/**
*
* Merge another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return PageCollection
*/
public function merge(PageCollectionInterface $collection)
{
return $this->getCollection()->merge($collection);
}
/**
* Intersect another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return PageCollection
*/
public function intersect(PageCollectionInterface $collection)
{
return $this->getCollection()->intersect($collection);
}
/**
* Split collection into array of smaller collections.
*
* @param int $size
* @return PageCollection[]
*/
public function batch($size)
{
return $this->getCollection()->batch($size);
}
/**
* Remove item from the list.
*
* @param PageInterface|string|null $key
*
* @return $this
* @throws InvalidArgumentException
*/
public function remove($key = null)
{
return $this->getCollection()->remove($key);
}
/**
* Reorder collection.
*
* @param string $by
* @param string $dir
* @param array $manual
* @param string $sort_flags
* @return static
*/
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('order', [$by, $dir, $manual, $sort_flags]);
return $collection;
}
/**
* Check to see if this item is the first in the collection.
*
* @param string $path
* @return bool True if item is first.
*/
public function isFirst($path): bool
{
/** @var bool $result */
$result = $this->__call('isFirst', [$path]);
return $result;
}
/**
* Check to see if this item is the last in the collection.
*
* @param string $path
* @return bool True if item is last.
*/
public function isLast($path): bool
{
/** @var bool $result */
$result = $this->__call('isLast', [$path]);
return $result;
}
/**
* Gets the previous sibling based on current position.
*
* @param string $path
* @return PageObject|null The previous item.
*/
public function prevSibling($path)
{
/** @var PageObject|null $result */
$result = $this->__call('prevSibling', [$path]);
return $result;
}
/**
* Gets the next sibling based on current position.
*
* @param string $path
* @return PageObject|null The next item.
*/
public function nextSibling($path)
{
/** @var PageObject|null $result */
$result = $this->__call('nextSibling', [$path]);
return $result;
}
/**
* Returns the adjacent sibling based on a direction.
*
* @param string $path
* @param int $direction either -1 or +1
* @return PageObject|false The sibling item.
*/
public function adjacentSibling($path, $direction = 1)
{
/** @var PageObject|false $result */
$result = $this->__call('adjacentSibling', [$path, $direction]);
return $result;
}
/**
* Returns the item in the current position.
*
* @param string $path the path the item
* @return int|null The index of the current page, null if not found.
*/
public function currentPosition($path): ?int
{
/** @var int|null $result */
$result = $this->__call('currentPosition', [$path]);
return $result;
}
/**
* Returns the items between a set of date ranges of either the page date field (default) or
* an arbitrary datetime page field where end date is optional
* Dates can be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param bool $endDate
* @param string|null $field
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
{
$collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
return $collection;
}
/**
* Mimicks Pages class.
*
* @return $this
* @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
*/
public function all()
{
return $this;
}
/**
* Creates new collection with only visible pages
*
* @return static The collection with only visible pages
*/
public function visible()
{
$collection = $this->__call('visible', []);
return $collection;
}
/**
* Creates new collection with only non-visible pages
*
* @return static The collection with only non-visible pages
*/
public function nonVisible()
{
$collection = $this->__call('nonVisible', []);
return $collection;
}
/**
* Creates new collection with only non-modular pages
*
* @return static The collection with only non-modular pages
*/
public function pages()
{
$collection = $this->__call('pages', []);
return $collection;
}
/**
* Creates new collection with only modular pages
*
* @return static The collection with only modular pages
*/
public function modules()
{
$collection = $this->__call('modules', []);
return $collection;
}
/**
* Creates new collection with only modular pages
*
* @return static The collection with only modular pages
*/
public function modular()
{
return $this->modules();
}
/**
* Creates new collection with only non-modular pages
*
* @return static The collection with only non-modular pages
*/
public function nonModular()
{
return $this->pages();
}
/**
* Creates new collection with only published pages
*
* @return static The collection with only published pages
*/
public function published()
{
$collection = $this->__call('published', []);
return $collection;
}
/**
* Creates new collection with only non-published pages
*
* @return static The collection with only non-published pages
*/
public function nonPublished()
{
$collection = $this->__call('nonPublished', []);
return $collection;
}
/**
* Creates new collection with only routable pages
*
* @return static The collection with only routable pages
*/
public function routable()
{
$collection = $this->__call('routable', []);
return $collection;
}
/**
* Creates new collection with only non-routable pages
*
* @return static The collection with only non-routable pages
*/
public function nonRoutable()
{
$collection = $this->__call('nonRoutable', []);
return $collection;
}
/**
* Creates new collection with only pages of the specified type
*
* @param string $type
* @return static The collection
*/
public function ofType($type)
{
$collection = $this->__call('ofType', []);
return $collection;
}
/**
* Creates new collection with only pages of one of the specified types
*
* @param string[] $types
* @return static The collection
*/
public function ofOneOfTheseTypes($types)
{
$collection = $this->__call('ofOneOfTheseTypes', []);
return $collection;
}
/**
* Creates new collection with only pages of one of the specified access levels
*
* @param array $accessLevels
* @return static The collection
*/
public function ofOneOfTheseAccessLevels($accessLevels)
{
$collection = $this->__call('ofOneOfTheseAccessLevels', []);
return $collection;
}
/**
* Converts collection into an array.
*
* @return array
*/
public function toArray()
{
return $this->getCollection()->toArray();
}
/**
* Get the extended version of this Collection with each page keyed by route
*
* @return array
* @throws Exception
*/
public function toExtendedArray()
{
return $this->getCollection()->toExtendedArray();
}
}
@@ -0,0 +1,628 @@
<?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\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,
] + parent::getCachedMethods();
}
/**
* @return void
*/
public function initialize(): void
{
if (!$this->_initialized) {
Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
$this->_initialized = true;
}
}
/**
* @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->getKey();
if ($key === $parentKey || strpos($parentKey, $key . '/') === 0) {
throw new RuntimeException(sprintf('Page /%s cannot be moved to %s', $key, $parentRoute));
}
/** @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]));
}
return $instance;
}
/**
* 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());
return $this;
}
/**
* @param array $ordering
* @return PageCollection
*/
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;
$parent = $this->parent();
if (!$parent) {
throw new RuntimeException('Cannot reorder a page which has no parent');
}
/** @var PageCollection|null $siblings */
$siblings = $parent->children();
/** @var PageCollection|null $siblings */
$siblings = $siblings->getCollection()->withOrdered()->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.");
}
}
$newSiblings = $newParent->children()->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.
$this->_reorder = !empty($elements['order']) ? explode(',', $elements['order']) : [];
$order = false;
if ((bool)($elements['ordering'] ?? false)) {
$order = 999999;
}
$this->order();
$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;
}
}
@@ -0,0 +1,668 @@
<?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();
}
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();
}
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('No storage key given');
}
$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) {
$this->copyRow($oldKey, $newKey);
$debugger->addMessage("Page copied: {$oldFolder} => {$newFolder}", 'debug');
} else {
$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) : $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');
}
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($newFolder)) {
$locator->clearCache();
}
} catch (RuntimeException $e) {
$name = isset($file) ? $file->filename() : $newKey;
throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $name, $e->getMessage()));
}
$row['__META'] = $this->getObjectMeta($newKey, true);
return $row;
}
/**
* @param string $key
* @return bool
*/
protected function canDeleteFolder(string $key): bool
{
$keys = $this->extractKeysFromStorageKey($key);
if ($keys['lang']) {
return false;
}
return true;
}
/**
* 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);
$path = $path ? $locator->findResource($path) : null;
} else {
$path = null;
}
$modified = 0;
$markdown = [];
$children = [];
if (is_string($path) && file_exists($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,120 @@
<?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\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();
$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($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<string,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<string,UserGroupObject,UserGroupCollection>
*/
class UserGroupIndex extends FlexIndex
{
}
@@ -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());
}
}
@@ -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<string,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);
}
}
@@ -0,0 +1,186 @@
<?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\UserInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Monolog\Logger;
use function count;
use function is_string;
use function method_exists;
/**
* Class UserIndex
* @package Grav\Common\Flex\Types\Users
*
* @extends FlexIndex<string,UserObject,UserCollection>
*/
class UserIndex extends FlexIndex
{
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;
}
/**
* 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');
}
}
@@ -0,0 +1,905 @@
<?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\Medium;
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');
if (null !== $password) {
$this->unsetProperty('password');
$this->unsetProperty('password1');
$this->unsetProperty('password2');
$this->setProperty('hashed_password', Authentication::create($password));
}
// 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();
$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, $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->_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
View 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;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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\Iterator;
/**
* Class AbstractCollection
* @package Grav\Common\GPM
*/
abstract class AbstractCollection extends Iterator
{
/**
* @return string
*/
public function toJson()
{
return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
}
/**
* @return array
*/
public function toArray()
{
$items = [];
foreach ($this->items as $name => $package) {
$items[$name] = $package->toArray();
}
return $items;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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 = [];
foreach ($this->items as $name => $package) {
$items[$name] = $package->toArray();
}
return json_encode($items, JSON_THROW_ON_ERROR);
}
/**
* @return array
*/
public function toArray()
{
$items = [];
foreach ($this->items as $name => $package) {
$items[$name] = $package->toArray();
}
return $items;
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Common;
use Grav\Common\Iterator;
/**
* 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[$method])) {
self::$cache[$method] = $items;
}
foreach (self::$cache[$method] as $name => $item) {
$this->append([$name => $item]);
}
}
}
@@ -0,0 +1,95 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Common;
use Grav\Common\Data\Data;
/**
* @property string $name
*/
class Package
{
/** @var Data */
protected $data;
/**
* Package constructor.
* @param Data $package
* @param string|null $type
*/
public function __construct(Data $package, $type = null)
{
$this->data = $package;
if ($type) {
$this->data->set('package_type', $type);
}
}
/**
* @return Data
*/
public function getData()
{
return $this->data;
}
/**
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->data->get($key);
}
/**
* @param string $key
* @param mixed $value
* @return void
*/
public function __set($key, $value)
{
$this->data->set($key, $value);
}
/**
* @param string $key
* @return bool
*/
public function __isset($key)
{
return isset($this->data->{$key});
}
/**
* @return string
*/
public function __toString()
{
return $this->toJson();
}
/**
* @return string
*/
public function toJson()
{
return $this->data->toJson();
}
/**
* @return array
*/
public function toArray()
{
return $this->data->toArray();
}
}
File diff suppressed because it is too large Load Diff
+540
View File
@@ -0,0 +1,540 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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 */
public const OK = 0;
/** @const Target already exists */
public const EXISTS = 1;
/** @const Target is a symbolic link */
public const IS_LINK = 2;
/** @const Target doesn't exist */
public const NOT_FOUND = 4;
/** @const Target is not a directory */
public const NOT_DIRECTORY = 8;
/** @const Target is not a Grav instance */
public const NOT_GRAV_ROOT = 16;
/** @const Error while trying to open the ZIP package */
public const ZIP_OPEN_ERROR = 32;
/** @const Error while trying to extract the ZIP package */
public const ZIP_EXTRACT_ERROR = 64;
/** @const Invalid source file */
public const INVALID_SOURCE = 128;
/** @var string Destination folder on which validation checks are applied */
protected static $target;
/** @var int|string Error code or string */
protected static $error = 0;
/** @var int Zip Error Code */
protected static $error_zip = 0;
/** @var string Post install message */
protected static $message = '';
/** @var array Default options for the install */
protected static $options = [
'overwrite' => true,
'ignore_symlinks' => true,
'sophisticated' => false,
'theme' => false,
'install_path' => '',
'ignores' => [],
'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK]
];
/**
* Installs a given package to a given destination.
*
* @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|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, $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']
)
) {
return false;
}
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('', false);
if (!$extracted) {
$extracted = self::unZip($zip, $tmp);
if (!$extracted) {
Folder::delete($tmp);
return false;
}
}
if (!file_exists($extracted)) {
self::$error = self::INVALID_SOURCE;
return false;
}
$is_install = true;
$installer = self::loadInstaller($extracted, $is_install);
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'preUpdate';
} else {
$method = 'preInstall';
}
if ($installer && method_exists($installer, $method)) {
$method_result = $installer::$method();
if ($method_result !== true) {
self::$error = 'An error occurred';
if (is_string($method_result)) {
self::$error = $method_result;
}
return false;
}
}
if (!$options['sophisticated']) {
if ($options['theme']) {
self::copyInstall($extracted, $install_path);
} else {
self::moveInstall($extracted, $install_path);
}
} else {
self::sophisticatedInstall($extracted, $install_path, $options['ignores'], $keepExtracted);
}
Folder::delete($tmp);
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'postUpdate';
} else {
$method = 'postInstall';
}
self::$message = '';
if ($installer && method_exists($installer, $method)) {
self::$message = $installer::$method();
}
self::$error = self::OK;
return true;
}
/**
* Unzip a file to somewhere
*
* @param string $zip_file
* @param string $destination
* @return string|false
*/
public static function unZip($zip_file, $destination)
{
$zip = new ZipArchive();
$archive = $zip->open($zip_file);
if ($archive === true) {
Folder::create($destination);
$unzip = $zip->extractTo($destination);
if (!$unzip) {
self::$error = self::ZIP_EXTRACT_ERROR;
Folder::delete($destination);
$zip->close();
return false;
}
$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();
return $destination . '/' . $package_folder_name;
}
self::$error = self::ZIP_EXTRACT_ERROR;
self::$error_zip = $archive;
return false;
}
/**
* Instantiates and returns the package installer class
*
* @param string $installer_file_folder The folder path that contains install.php
* @param bool $is_install True if install, false if removal
* @return string|null
*/
private static function loadInstaller($installer_file_folder, $is_install)
{
$installer_file_folder = rtrim($installer_file_folder, DS);
$install_file = $installer_file_folder . DS . 'install.php';
if (!file_exists($install_file)) {
return null;
}
require_once $install_file;
if ($is_install) {
$slug = '';
if (($pos = strpos($installer_file_folder, 'grav-plugin-')) !== false) {
$slug = substr($installer_file_folder, $pos + strlen('grav-plugin-'));
} elseif (($pos = strpos($installer_file_folder, 'grav-theme-')) !== false) {
$slug = substr($installer_file_folder, $pos + strlen('grav-theme-'));
}
} else {
$path_elements = explode('/', $installer_file_folder);
$slug = end($path_elements);
}
if (!$slug) {
return null;
}
$class_name = ucfirst($slug) . 'Install';
if (class_exists($class_name)) {
return $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 null;
}
/**
* @param string $source_path
* @param string $install_path
* @return bool
*/
public static function moveInstall($source_path, $install_path)
{
if (file_exists($install_path)) {
Folder::delete($install_path);
}
Folder::move($source_path, $install_path);
return true;
}
/**
* @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");
}
Folder::rcopy($source_path, $install_path);
return true;
}
/**
* @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 = [], $keep_source = false)
{
foreach (new DirectoryIterator($source_path) as $file) {
if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores, true)) {
continue;
}
$path = $install_path . DS . $file->getFilename();
if ($file->isDir()) {
Folder::delete($path);
if ($keep_source) {
Folder::copy($file->getPathname(), $path);
} else {
Folder::move($file->getPathname(), $path);
}
if ($file->getFilename() === 'bin') {
$glob = glob($path . DS . '*') ?: [];
foreach ($glob as $bin_file) {
@chmod($bin_file, 0755);
}
}
} else {
@unlink($path);
@copy($file->getPathname(), $path);
}
}
return true;
}
/**
* Uninstalls one or more given package
*
* @param string $path The slug of the package(s)
* @param array $options Options to use for uninstalling
* @return bool True if everything went fine, False otherwise.
*/
public static function uninstall($path, $options = [])
{
$options = array_merge(self::$options, $options);
if (!self::isValidDestination($path, $options['exclude_checks'])
) {
return false;
}
$installer_file_folder = $path;
$is_install = false;
$installer = self::loadInstaller($installer_file_folder, $is_install);
if ($installer && method_exists($installer, 'preUninstall')) {
$method_result = $installer::preUninstall();
if ($method_result !== true) {
self::$error = 'An error occurred';
if (is_string($method_result)) {
self::$error = $method_result;
}
return false;
}
}
$result = Folder::delete($path);
self::$message = '';
if ($result && $installer && method_exists($installer, 'postUninstall')) {
self::$message = $installer::postUninstall();
}
return $result;
}
/**
* Runs a set of checks on the destination and sets the Error if any
*
* @param string $destination The directory to run validations at
* @param array $exclude An array of constants to exclude from the validation
* @return bool True if validation passed. False otherwise
*/
public static function isValidDestination($destination, $exclude = [])
{
self::$error = 0;
self::$target = $destination;
if (is_link($destination)) {
self::$error = self::IS_LINK;
} elseif (file_exists($destination)) {
self::$error = self::EXISTS;
} elseif (!file_exists($destination)) {
self::$error = self::NOT_FOUND;
} elseif (!is_dir($destination)) {
self::$error = self::NOT_DIRECTORY;
}
if (count($exclude) && in_array(self::$error, $exclude, true)) {
return true;
}
return !self::$error;
}
/**
* Validates if the given path is a Grav Instance
*
* @param string $target The local path to the Grav Instance
* @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') ||
!file_exists($target . DS . 'bin') ||
!file_exists($target . DS . 'user') ||
!file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml')
) {
self::$error = self::NOT_GRAV_ROOT;
}
return !self::$error;
}
/**
* Returns the last message added by the installer
*
* @return string The message
*/
public static function getMessage()
{
return self::$message;
}
/**
* Returns the last error occurred in a string message format
*
* @return string The message of the last error
*/
public static function lastErrorMsg()
{
if (is_string(self::$error)) {
return self::$error;
}
switch (self::$error) {
case 0:
$msg = 'No Error';
break;
case self::EXISTS:
$msg = 'The target path "' . self::$target . '" already exists';
break;
case self::IS_LINK:
$msg = 'The target path "' . self::$target . '" is a symbolic link';
break;
case self::NOT_FOUND:
$msg = 'The target path "' . self::$target . '" does not appear to exist';
break;
case self::NOT_DIRECTORY:
$msg = 'The target path "' . self::$target . '" does not appear to be a folder';
break;
case self::NOT_GRAV_ROOT:
$msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance';
break;
case self::ZIP_OPEN_ERROR:
$msg = 'Unable to open the package file';
break;
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.';
break;
case ZipArchive::ER_INCONS:
$msg .= 'Zip archive inconsistent.';
break;
case ZipArchive::ER_MEMORY:
$msg .= 'Memory allocation failure.';
break;
case ZipArchive::ER_NOENT:
$msg .= 'No such file.';
break;
case ZipArchive::ER_NOZIP:
$msg .= 'Not a zip archive.';
break;
case ZipArchive::ER_OPEN:
$msg .= "Can't open file.";
break;
case ZipArchive::ER_READ:
$msg .= 'Read error.';
break;
case ZipArchive::ER_SEEK:
$msg .= 'Seek error.';
break;
}
}
break;
case self::INVALID_SOURCE:
$msg = 'Invalid source file';
break;
default:
$msg = 'Unknown Error';
break;
}
return $msg;
}
/**
* Returns the last error code of the occurred error
*
* @return int|string The code of the last error
*/
public static function lastErrorCode()
{
return self::$error;
}
/**
* Allows to manually set an error
*
* @param int|string $error the Error code
* @return void
*/
public static function setError($error)
{
self::$error = $error;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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\File\CompiledYamlFile;
use Grav\Common\Grav;
use RocketTheme\Toolbox\File\FileInterface;
use function is_string;
/**
* Class Licenses
*
* @package Grav\Common\GPM
*/
class Licenses
{
/** @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 string $slug
* @param string $license
* @return bool
*/
public static function set($slug, $license)
{
$licenses = self::getLicenseFile();
$data = (array)$licenses->content();
$slug = strtolower($slug);
if ($license && !self::validate($license)) {
return false;
}
if (!is_string($license)) {
if (isset($data['licenses'][$slug])) {
unset($data['licenses'][$slug]);
} else {
return false;
}
} else {
$data['licenses'][$slug] = $license;
}
$licenses->save($data);
$licenses->free();
return true;
}
/**
* Returns the license for a Premium package
*
* @param string|null $slug
* @return array|string
*/
public static function get($slug = null)
{
$licenses = self::getLicenseFile();
$data = (array)$licenses->content();
$licenses->free();
if (null === $slug) {
return $data['licenses'] ?? [];
}
$slug = strtolower($slug);
return $data['licenses'][$slug] ?? '';
}
/**
* Validates the License format
*
* @param string|null $license
* @return bool
*/
public static function validate($license = null)
{
if (!is_string($license)) {
return false;
}
return (bool)preg_match('#' . self::$regex. '#', $license);
}
/**
* Get the License File object
*
* @return FileInterface
*/
public static function getLicenseFile()
{
if (!isset(self::$file)) {
$path = Grav::instance()['locator']->findResource('user-data://') . '/licenses.yaml';
if (!file_exists($path)) {
touch($path);
}
self::$file = CompiledYamlFile::instance($path);
}
return self::$file;
}
}
@@ -0,0 +1,34 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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);
}
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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());
parent::__construct($data, $package_type);
$this->settings = $package->toArray();
$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->__get('slug')));
}
/**
* @return bool
*/
public function isEnabled()
{
return (bool)$this->settings['enabled'];
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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()
{
$items = [
'plugins' => new Plugins(),
'themes' => new Themes()
];
parent::__construct($items);
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Local;
use Grav\Common\Grav;
/**
* Class Plugins
* @package Grav\Common\GPM\Local
*/
class Plugins extends AbstractPackageCollection
{
/** @var string */
protected $type = 'plugins';
/**
* Local Plugins Constructor
*/
public function __construct()
{
/** @var \Grav\Common\Plugins $plugins */
$plugins = Grav::instance()['plugins'];
parent::__construct($plugins->all());
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Local;
use Grav\Common\Grav;
/**
* Class Themes
* @package Grav\Common\GPM\Local
*/
class Themes extends AbstractPackageCollection
{
/** @var string */
protected $type = 'themes';
/**
* Local Themes Constructor
*/
public function __construct()
{
/** @var \Grav\Common\Themes $themes */
$themes = Grav::instance()['themes'];
parent::__construct($themes->all());
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Remote;
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
{
/** @var string The cached data previously fetched */
protected $raw;
/** @var string */
protected $repository;
/** @var FilesystemCache */
protected $cache;
/** @var int The lifetime to store the entry in seconds */
private $lifetime = 86400;
/**
* AbstractPackageCollection constructor.
*
* @param string|null $repository
* @param bool $refresh
* @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');
}
$channel = Grav::instance()['config']->get('system.gpm.releases', 'stable');
$cache_dir = Grav::instance()['locator']->findResource('cache://gpm', true, true);
$this->cache = new FilesystemCache($cache_dir);
$this->repository = $repository . '?v=' . GRAV_VERSION . '&' . $channel . '=1';
$this->raw = $this->cache->fetch(md5($this->repository));
$this->fetch($refresh, $callback);
foreach (json_decode($this->raw, true) as $slug => $data) {
// 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);
}
$this->items[$slug] = new Package($data, $this->type);
}
}
/**
* @param bool $refresh
* @param callable|null $callback
* @return string
*/
public function fetch($refresh = false, $callback = null)
{
if (!$this->raw || $refresh) {
$response = Response::get($this->repository, [], $callback);
$this->raw = $response;
$this->cache->save(md5($this->repository), $this->raw, $this->lifetime);
}
return $this->raw;
}
}
@@ -0,0 +1,151 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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';
/** @var array */
private $data;
/** @var string */
private $version;
/** @var string */
private $date;
/** @var string|null */
private $min_php;
/**
* @param bool $refresh
* @param callable|null $callback
* @throws InvalidArgumentException
*/
public function __construct($refresh = false, $callback = null)
{
$channel = Grav::instance()['config']->get('system.gpm.releases', 'stable');
$cache_dir = Grav::instance()['locator']->findResource('cache://gpm', true, true);
$this->cache = new FilesystemCache($cache_dir);
$this->repository .= '?v=' . GRAV_VERSION . '&' . $channel . '=1';
$this->raw = $this->cache->fetch(md5($this->repository));
$this->fetch($refresh, $callback);
$this->data = json_decode($this->raw, true);
$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) {
$this->items[$slug] = new Package($data);
}
}
}
/**
* Returns the list of assets associated to the latest version of Grav
*
* @return array list of assets
*/
public function getAssets()
{
return $this->data['assets'];
}
/**
* Returns the changelog list for each version of Grav
*
* @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;
}
/**
* Return the release date of the latest Grav
*
* @return string
*/
public function getDate()
{
return $this->date;
}
/**
* Determine if this version of Grav is eligible to be updated
*
* @return mixed
*/
public function isUpdatable()
{
return version_compare(GRAV_VERSION, $this->getVersion(), '<');
}
/**
* Returns the latest version of Grav available remotely
*
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* Returns the minimum PHP version
*
* @return string
*/
public function getMinPHPVersion()
{
// If non min set, assume current PHP version
if (null === $this->min_php) {
$this->min_php = PHP_VERSION;
}
return $this->min_php;
}
/**
* Is this installation symlinked?
*
* @return bool
*/
public function isSymlink()
{
return is_link(GRAV_ROOT . DS . 'index.php');
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Remote;
use Grav\Common\Data\Data;
use Grav\Common\GPM\Common\Package as BasePackage;
/**
* 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;
}
}
@@ -0,0 +1,34 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
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 = [
'plugins' => new Plugins($refresh, $callback),
'themes' => new Themes($refresh, $callback)
];
parent::__construct($items);
}
}
@@ -0,0 +1,32 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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 */
protected $type = 'plugins';
/** @var string */
protected $repository = 'https://getgrav.org/downloads/plugins.json';
/**
* Local Plugins Constructor
* @param bool $refresh
* @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
{
parent::__construct($this->repository, $refresh, $callback);
}
}
@@ -0,0 +1,32 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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 */
protected $type = 'themes';
/** @var string */
protected $repository = 'https://getgrav.org/downloads/themes.json';
/**
* Local Themes Constructor
* @param bool $refresh
* @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
{
parent::__construct($this->repository, $refresh, $callback);
}
}
+143
View File
@@ -0,0 +1,143 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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
{
/** @var callable The callback for the progress, either a function or callback in array notation */
public static $callback = null;
/** @var string[] */
private static $headers = [
'User-Agent' => 'Grav CMS'
];
/**
* Makes a request to the URL by using the preferred method
*
* @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 = '', $overrides = [], $callback = null)
{
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::functionExists('set_time_limit')) {
set_time_limit(0);
}
} catch (Exception $e) {
}
$config = Grav::instance()['config'];
$referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
$options = new HttpOptions();
// 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);
}
// Set proxy url if provided
$proxy_url = $config->get('system.gpm.proxy_url', false);
if ($proxy_url) {
$options->setProxy($proxy_url);
}
// Use callback if provided
if ($callback) {
self::$callback = $callback;
$options->setOnProgress([Response::class, 'progress']);
}
$preferred_method = $config->get('system.gpm.method', 'auto');
$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();
}
/**
* Is this a remote file or not
*
* @param string $file
* @return bool
*/
public static function isRemote($file)
{
return (bool) filter_var($file, FILTER_VALIDATE_URL);
}
/**
* Progress normalized for cURL and Fopen
* Accepts a variable length of arguments passed in by stream method
*
* @return void
*/
public static function progress(int $bytes_transferred, int $filesize, array $info)
{
if ($bytes_transferred > 0) {
$percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize);
$progress = [
'code' => $info['http_code'],
'filesize' => $filesize,
'transferred' => $bytes_transferred,
'percent' => $percent < 100 ? $percent : 100
];
if (self::$callback !== null) {
call_user_func(self::$callback, $progress);
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
/**
* @package Grav\Common\GPM
*
* @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
*
* @package Grav\Common\GPM
*/
class Upgrader
{
/** @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|null $callback Either a function or callback in array notation
* @throws InvalidArgumentException
*/
public function __construct($refresh = false, $callback = null)
{
$this->remote = new Remote\GravCore($refresh, $callback);
}
/**
* Returns the release date of the latest version of Grav
*
* @return string
*/
public function getReleaseDate()
{
return $this->remote->getDate();
}
/**
* Returns the version of the installed Grav
*
* @return string
*/
public function getLocalVersion()
{
return GRAV_VERSION;
}
/**
* Returns the version of the remotely available Grav
*
* @return string
*/
public function getRemoteVersion()
{
return $this->remote->getVersion();
}
/**
* Returns an array of assets available to download remotely
*
* @return array
*/
public function getAssets()
{
return $this->remote->getAssets();
}
/**
* Returns the changelog list for each version of Grav
*
* @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)
{
return $this->remote->getChangelog($diff);
}
/**
* Make sure this meets minimum PHP requirements
*
* @return bool
*/
public function meetsRequirements()
{
if (version_compare(PHP_VERSION, $this->minPHPVersion(), '<')) {
return false;
}
return true;
}
/**
* Get minimum PHP version from remote
*
* @return string
*/
public function minPHPVersion()
{
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 bool True if it's upgradable, False otherwise.
*/
public function isUpgradable()
{
return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), '<');
}
/**
* Checks if Grav is currently symbolically linked
*
* @return bool True if Grav is symlinked, False otherwise.
*/
public function isSymlink()
{
return $this->remote->isSymlink();
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
/**
* @package Grav\Common
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use ArrayAccess;
use Countable;
use function count;
/**
* Class Getters
* @package Grav\Common
*/
abstract class Getters implements ArrayAccess, Countable
{
/** @var string Define variable used in getters. */
protected $gettersVariable = null;
/**
* Magic setter method
*
* @param int|string $offset Medium name value
* @param mixed $value Medium value
*/
public function __set($offset, $value)
{
$this->offsetSet($offset, $value);
}
/**
* Magic getter method
*
* @param int|string $offset Medium name value
* @return mixed Medium value
*/
public function __get($offset)
{
return $this->offsetGet($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param int|string $offset Medium name value
* @return boolean True if the value is set
*/
public function __isset($offset)
{
return $this->offsetExists($offset);
}
/**
* Magic method to unset the attribute
*
* @param int|string $offset The name value to unset
*/
public function __unset($offset)
{
$this->offsetUnset($offset);
}
/**
* @param int|string $offset
* @return bool
*/
public function offsetExists($offset)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return isset($this->{$var}[$offset]);
}
return isset($this->{$offset});
}
/**
* @param int|string $offset
* @return mixed
*/
public function offsetGet($offset)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return $this->{$var}[$offset] ?? null;
}
return $this->{$offset} ?? null;
}
/**
* @param int|string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
$this->{$var}[$offset] = $value;
} else {
$this->{$offset} = $value;
}
}
/**
* @param int|string $offset
*/
public function offsetUnset($offset)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
unset($this->{$var}[$offset]);
} else {
unset($this->{$offset});
}
}
/**
* @return int
*/
public function count()
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return count($this->{$var});
}
return count($this->toArray());
}
/**
* Returns an associative array of object properties.
*
* @return array
*/
public function toArray()
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return $this->{$var};
}
$properties = (array)$this;
$list = [];
foreach ($properties as $property => $value) {
if ($property[0] !== "\0") {
$list[$property] = $value;
}
}
return $list;
}
}
+772
View File
@@ -0,0 +1,772 @@
<?php
/**
* @package Grav\Common
*
* @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\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 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. */
public $output;
/** @var static The singleton instance */
protected static $instance;
/**
* @var array Contains all Services and ServicesProviders that are mapped
* to the dependency injection container.
*/
protected static $diMap = [
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 middleware processors that are processed in $this->process()
*/
protected $middleware = [
'initializeProcessor',
'pluginsProcessor',
'themesProcessor',
'requestProcessor',
'tasksProcessor',
'backupsProcessor',
'schedulerProcessor',
'assetsProcessor',
'twigProcessor',
'pagesProcessor',
'debuggerAssetsProcessor',
'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;
}
}
/**
* Return the Grav instance. Create it if it's not already instanced
*
* @param array $values
* @return Grav
*/
public static function instance(array $values = [])
{
if (null === self::$instance) {
self::$instance = static::load($values);
} elseif ($values) {
$instance = self::$instance;
foreach ($values as $key => $value) {
$instance->offsetSet($key, $value);
}
}
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()
{
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'];
$response = $debugger->logRequest($request, $response);
$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);
// 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];
}
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()
{
// Initialize Locale if set and configured.
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
$language = $this['language']->getLanguage();
setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
} elseif ($this['config']->get('system.default_locale')) {
setlocale(LC_ALL, $this['config']->get('system.default_locale'));
}
}
/**
* @param object $event
* @return object
*/
public function dispatchEvent($event)
{
/** @var EventDispatcherInterface $events */
$events = $this['events'];
$eventName = get_class($event);
$timestamp = microtime(true);
$event = $events->dispatch($event);
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$debugger->addEvent($eventName, $event, $events, $timestamp);
return $event;
}
/**
* Fires an event with optional parameters.
*
* @param string $eventName
* @param Event|null $event
* @return Event
*/
public function fireEvent($eventName, Event $event = null)
{
/** @var EventDispatcherInterface $events */
$events = $this['events'];
if (null === $event) {
$event = new 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()
{
// Prevent user abort allowing onShutdown event to run without interruptions.
if (function_exists('ignore_user_abort')) {
@ignore_user_abort(true);
}
// Close the session allowing new requests to be handled.
if (isset($this['session'])) {
$this['session']->close();
}
/** @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 ($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.
@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');
ob_end_flush();
@ob_flush();
flush();
}
}
// Run any time consuming tasks.
$this->fireEvent('onShutdown');
}
/**
* Magic Catch All Function
*
* 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} ?? 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['debugger'] = new 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);
return $container;
};
$container->registerServices();
return $container;
}
/**
* Register all services
* Services are defined in the diMap. They can either only the class
* of a Service Provider or a pair of serviceKey => serviceClass that
* gets directly mapped into the container.
*
* @return void
*/
protected function registerServices()
{
foreach (self::$diMap as $serviceKey => $serviceClass) {
if (is_int($serviceKey)) {
$this->register(new $serviceClass);
} else {
$this[$serviceKey] = function ($c) use ($serviceClass) {
return new $serviceClass($c);
};
}
}
}
/**
* This attempts to find media, other files, and download them
*
* @param string $path
* @return PageInterface|false
*/
public function fallbackUrl($path)
{
$this->fireEvent('onPageFallBackUrl');
/** @var Uri $uri */
$uri = $this['uri'];
/** @var Config $config */
$config = $this['config'];
$uri_extension = strtolower($uri->extension());
$fallback_types = $config->get('system.media.allowed_fallback_types', null);
$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)) {
return false;
}
if (!array_key_exists($uri_extension, $supported_types)) {
return false;
}
$path_parts = pathinfo($path);
/** @var Pages $pages */
$pages = $this['pages'];
$page = $pages->find($path_parts['dirname'], true);
if ($page) {
$media = $page->media()->all();
$parsed_url = parse_url(rawurldecode($uri->basename()));
$media_file = $parsed_url['path'];
// if this is a media object, try actions first
if (isset($media[$media_file])) {
/** @var Medium $medium */
$medium = $media[$media_file];
foreach ($uri->query(null, true) as $action => $params) {
if (in_array($action, ImageMedium::$magic_actions, true)) {
call_user_func_array([&$medium, $action], explode(',', $params));
}
}
Utils::download($medium->path(), false);
}
// unsupported media type, try to download it...
if ($uri_extension) {
$extension = $uri_extension;
} else {
if (isset($path_parts['extension'])) {
$extension = $path_parts['extension'];
} else {
$extension = null;
}
}
if ($extension) {
$download = true;
if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
$download = false;
}
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
}
// Nothing found
return false;
}
return $page;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* @package Grav\Common
*
* @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.
*/
trait GravTrait
{
/** @var Grav */
protected static $grav;
/**
* @return Grav
* @deprecated 1.4 Use Grav::instance() instead.
*/
public static function getGrav()
{
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
if (null === self::$grav) {
self::$grav = Grav::instance();
}
return self::$grav;
}
}
+141
View File
@@ -0,0 +1,141 @@
<?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 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'
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', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
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 string $bytes
* @return string
*/
public static function encode($bytes)
{
$i = 0;
$index = 0;
$base32 = '';
$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) < $bytesLen) {
$nextByte = ord($bytes[$i+1]);
} else {
$nextByte = 0;
}
$digit = $currByte & (0xFF >> $index);
$index = ($index + 5) % 8;
$digit <<= $index;
$digit |= $nextByte >> (8 - $index);
$i++;
} else {
$digit = ($currByte >> (8 - ($index + 5))) & 0x1F;
$index = ($index + 5) % 8;
if ($index === 0) {
$i++;
}
}
$base32 .= self::$base32Chars[$digit];
}
return $base32;
}
/**
* Decode in Base32
*
* @param string $base32
* @return string
*/
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 < $base32Len; $i++) {
$lookup = ord($base32[$i]) - ord('0');
/* Skip chars outside the lookup table */
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) {
$index = ($index + 5) % 8;
if ($index === 0) {
$bytes[$offset] |= $digit;
$offset++;
if ($offset >= count($bytes)) {
break;
}
} else {
$bytes[$offset] |= $digit << (8 - $index);
}
} else {
$index = ($index + 5) % 8;
$bytes[$offset] |= ($digit >> $index);
$offset++;
if ($offset >= count($bytes)) {
break;
}
$bytes[$offset] |= $digit << (8 - $index);
}
}
$bites = '';
foreach ($bytes as $byte) {
$bites .= chr($byte);
}
return $bites;
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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 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 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 PageInterface|null $page Page, defaults to the current page object
* @return string Returns final HTML string
*/
public static function processImageHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
$original_src = $excerpt['element']['attributes']['src'];
$excerpt['element']['attributes']['href'] = $original_src;
$excerpt = static::processLinkExcerpt($excerpt, $page, 'image');
$excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href'];
unset($excerpt['element']['attributes']['href']);
$excerpt = static::processImageExcerpt($excerpt, $page);
$excerpt['element']['attributes']['data-src'] = $original_src;
$html = static::getHtmlFromExcerpt($excerpt);
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
*
* @param string $html Chunk of HTML
* @param string $tag A tag, for example `img`
* @return array|null returns nested array excerpt
*/
public static function getExcerptFromHtml($html, $tag)
{
$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);
$elements = $doc->getElementsByTagName($tag);
$excerpt = null;
$inner = [];
/** @var DOMElement $element */
foreach ($elements as $element) {
$attributes = [];
foreach ($element->attributes as $name => $value) {
$attributes[$name] = $value->value;
}
$excerpt = [
'element' => [
'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;
}
/**
* Rebuild HTML tag from an excerpt array
*
* @param array $excerpt
* @return string
*/
public static function getHtmlFromExcerpt($excerpt)
{
$element = $excerpt['element'];
$html = '<'.$element['name'];
if (isset($element['attributes'])) {
foreach ($element['attributes'] as $name => $value) {
if ($value === null) {
continue;
}
$html .= ' '.$name.'="'.$value.'"';
}
}
if (isset($element['text'])) {
$html .= '>';
$html .= is_array($element['text']) ? static::getHtmlFromExcerpt(['element' => $element['text']]) : $element['text'];
$html .= '</'.$element['name'].'>';
} else {
$html .= ' />';
}
return $html;
}
/**
* Process a Link excerpt
*
* @param array $excerpt
* @param PageInterface|null $page Page, defaults to the current page object
* @param string $type
* @return mixed
*/
public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link')
{
$excerpts = new ExcerptsObject($page);
return $excerpts->processLinkExcerpt($excerpt, $type);
}
/**
* Process an image excerpt
*
* @param array $excerpt
* @param PageInterface|null $page Page, defaults to the current page object
* @return array
*/
public static function processImageExcerpt(array $excerpt, PageInterface $page = null)
{
$excerpts = new ExcerptsObject($page);
return $excerpts->processImageExcerpt($excerpt);
}
/**
* Process media actions
*
* @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, PageInterface $page = null)
{
$excerpts = new ExcerptsObject($page);
return $excerpts->processMediaActions($medium, $url);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?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 Grav\Common\Grav;
use PHPExif\Reader\Reader;
use RuntimeException;
use function function_exists;
/**
* Class Exif
* @package Grav\Common\Helpers
*/
class Exif
{
/** @var Reader */
public $reader;
/**
* Exif constructor.
* @throws RuntimeException
*/
public function __construct()
{
if (Grav::instance()['config']->get('system.media.auto_metadata_exif')) {
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');
}
}
}
/**
* @return Reader
*/
public function getReader()
{
return $this->reader;
}
}

Some files were not shown because too many files have changed in this diff Show More