first commit
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
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;
|
||||
|
||||
class Assets extends PropertyObject
|
||||
{
|
||||
use TestingAssetsTrait;
|
||||
use LegacyAssetsTrait;
|
||||
|
||||
const CSS_COLLECTION = 'assets_css';
|
||||
const JS_COLLECTION = 'assets_js';
|
||||
const CSS_TYPE = 'Css';
|
||||
const JS_TYPE = 'Js';
|
||||
const INLINE_CSS_TYPE = 'InlineCss';
|
||||
const INLINE_JS_TYPE = 'InlineJs';
|
||||
|
||||
/** @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';
|
||||
|
||||
protected $assets_dir;
|
||||
protected $assets_url;
|
||||
|
||||
protected $assets_css = [];
|
||||
protected $assets_js = [];
|
||||
|
||||
// Config Options
|
||||
protected $css_pipeline;
|
||||
protected $css_pipeline_include_externals;
|
||||
protected $css_pipeline_before_excludes;
|
||||
protected $js_pipeline;
|
||||
protected $js_pipeline_include_externals;
|
||||
protected $js_pipeline_before_excludes;
|
||||
protected $pipeline_options = [];
|
||||
|
||||
|
||||
protected $fetch_command;
|
||||
protected $autoload;
|
||||
protected $enable_asset_timestamp;
|
||||
protected $collections;
|
||||
protected $timestamp;
|
||||
|
||||
|
||||
/**
|
||||
* Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
|
||||
*/
|
||||
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 array|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 (\strlen($extension) > 0) {
|
||||
$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;
|
||||
}
|
||||
|
||||
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_TYPE : $this::CSS_TYPE;
|
||||
$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
|
||||
$options['order'] = \count($this->$collection);
|
||||
|
||||
// Create asset of correct type
|
||||
$asset_class = "\\Grav\\Common\\Assets\\{$type}";
|
||||
$asset_object = new $asset_class();
|
||||
|
||||
// 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(Assets::CSS_COLLECTION,Assets::CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::CSS_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Inline CSS asset or a collection of assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addInlineCss($asset)
|
||||
{
|
||||
return $this->addType(Assets::CSS_COLLECTION, Assets::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_CSS_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a JS asset or a collection of assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addJs($asset)
|
||||
{
|
||||
return $this->addType(Assets::JS_COLLECTION, Assets::JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::JS_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Inline JS asset or a collection of assets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addInlineJs($asset)
|
||||
{
|
||||
return $this->addType(Assets::JS_COLLECTION, Assets::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::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;
|
||||
}
|
||||
|
||||
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->{$type . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline' ) {
|
||||
if ($this->{$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;
|
||||
}
|
||||
|
||||
protected function sortAssets($assets)
|
||||
{
|
||||
uasort ($assets, function($a, $b) {
|
||||
if ($a['priority'] == $b['priority']) {
|
||||
return $a['order'] - $b['order'];
|
||||
}
|
||||
return $b['priority'] - $a['priority'];
|
||||
});
|
||||
return $assets;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
abstract class BaseAsset extends PropertyObject
|
||||
{
|
||||
use AssetUtilsTrait;
|
||||
|
||||
protected const CSS_ASSET = true;
|
||||
protected const JS_ASSET = false;
|
||||
|
||||
/** @const Regex to match CSS import content */
|
||||
protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
|
||||
|
||||
protected $asset;
|
||||
|
||||
protected $asset_type;
|
||||
protected $order;
|
||||
protected $group;
|
||||
protected $position;
|
||||
protected $priority;
|
||||
protected $attributes = [];
|
||||
|
||||
|
||||
protected $timestamp;
|
||||
protected $modified;
|
||||
protected $remote;
|
||||
protected $query = '';
|
||||
|
||||
// Private Bits
|
||||
private $base_url;
|
||||
private $fetch_command;
|
||||
private $css_rewrite = false;
|
||||
private $css_minify = false;
|
||||
|
||||
abstract function render();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getAsset()
|
||||
{
|
||||
return $this->asset;
|
||||
}
|
||||
|
||||
public function getRemote()
|
||||
{
|
||||
return $this->remote;
|
||||
}
|
||||
|
||||
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 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
|
||||
*/
|
||||
protected function cssRewrite($file, $dir, $local)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class Css extends BaseAsset
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class InlineCss extends BaseAsset
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<style' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</style>\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class InlineJs extends BaseAsset
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<script' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</script>\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class Js extends BaseAsset
|
||||
{
|
||||
public function __construct(array $elements = [], $key = null)
|
||||
{
|
||||
$base_options = [
|
||||
'asset_type' => 'js',
|
||||
];
|
||||
|
||||
$merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
|
||||
|
||||
parent::__construct($merged_attributes, $key);
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
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 = '{\/\*# (.*?) \*\/}';
|
||||
|
||||
/** @const Regex to match CSS import content */
|
||||
protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
|
||||
|
||||
protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
|
||||
|
||||
protected $css_minify;
|
||||
protected $css_minify_windows;
|
||||
protected $css_rewrite;
|
||||
|
||||
protected $js_minify;
|
||||
protected $js_minify_windows;
|
||||
|
||||
protected $base_url;
|
||||
protected $assets_dir;
|
||||
protected $assets_url;
|
||||
protected $timestamp;
|
||||
protected $attributes;
|
||||
protected $query;
|
||||
protected $asset;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $fetch_command;
|
||||
|
||||
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 \MatthiasMullie\Minify\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 \MatthiasMullie\Minify\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 mixed
|
||||
*/
|
||||
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;
|
||||
|
||||
$fixed = str_replace($matches[2], $new_url, $matches[0]);
|
||||
|
||||
return $fixed;
|
||||
}, $file);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
||||
|
||||
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,182 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
trait AssetUtilsTrait
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
$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)
|
||||
{
|
||||
$imports = [];
|
||||
|
||||
$file = (string)preg_replace_callback(self::CSS_IMPORT_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 $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,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use Grav\Common\Assets;
|
||||
|
||||
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'];
|
||||
$arguments = array_merge($arguments, $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;
|
||||
}
|
||||
|
||||
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 \Grav\Common\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 \Grav\Common\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,343 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Assets\Traits
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Assets\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
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 null|string $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 null|string $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);
|
||||
|
||||
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
|
||||
*/
|
||||
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 $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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Backup
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Backup;
|
||||
|
||||
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\Event\EventDispatcher;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
class Backups
|
||||
{
|
||||
protected const BACKUP_FILENAME_REGEXZ = "#(.*)--(\d*).zip#";
|
||||
|
||||
protected const BACKUP_DATE_FORMAT = 'YmdHis';
|
||||
|
||||
protected static $backup_dir;
|
||||
|
||||
protected static $backups = null;
|
||||
|
||||
public function init()
|
||||
{
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = Grav::instance()['events'];
|
||||
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
|
||||
Grav::instance()->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
|
||||
}
|
||||
|
||||
public function setup()
|
||||
{
|
||||
if (null === static::$backup_dir) {
|
||||
static::$backup_dir = Grav::instance()['locator']->findResource('backup://', true, true);
|
||||
Folder::create(static::$backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
public function onSchedulerInitialized(Event $event)
|
||||
{
|
||||
/** @var Scheduler $scheduler */
|
||||
$scheduler = $event['scheduler'];
|
||||
|
||||
/** @var Inflector $inflector */
|
||||
$inflector = Grav::instance()['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');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBackupDownloadUrl($backup, $base_url)
|
||||
{
|
||||
$param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
|
||||
$download = urlencode(base64_encode($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;
|
||||
}
|
||||
|
||||
public static function getBackupProfiles()
|
||||
{
|
||||
return Grav::instance()['config']->get('backups.profiles');
|
||||
}
|
||||
|
||||
public static function getPurgeConfig()
|
||||
{
|
||||
return Grav::instance()['config']->get('backups.purge');
|
||||
}
|
||||
|
||||
public function getBackupNames()
|
||||
{
|
||||
return array_column(static::getBackupProfiles(), 'name');
|
||||
}
|
||||
|
||||
public static function getTotalBackupsSize()
|
||||
{
|
||||
$backups = static::getAvailableBackups();
|
||||
$size = array_sum(array_column($backups, 'size'));
|
||||
|
||||
return $size ?? 0;
|
||||
}
|
||||
|
||||
public static function getAvailableBackups($force = false)
|
||||
{
|
||||
if ($force || null === static::$backups) {
|
||||
static::$backups = [];
|
||||
$backups_itr = new \GlobIterator(static::$backup_dir . '/*.zip', \FilesystemIterator::KEY_AS_FILENAME);
|
||||
$inflector = Grav::instance()['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 null|string
|
||||
*/
|
||||
public static function backup($id = 0, callable $status = null)
|
||||
{
|
||||
$profiles = static::getBackupProfiles();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if (isset($profiles[$id])) {
|
||||
$backup = (object) $profiles[$id];
|
||||
} else {
|
||||
throw new \RuntimeException('No backups defined...');
|
||||
}
|
||||
|
||||
$name = Grav::instance()['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 ?? ''),
|
||||
];
|
||||
|
||||
/** @var Archiver $archiver */
|
||||
$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::instance()['log']->notice('Backup Created: ' . $destination);
|
||||
|
||||
// Fire Finished event
|
||||
Grav::instance()->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
|
||||
|
||||
// Purge anything required
|
||||
static::purge();
|
||||
|
||||
// Log
|
||||
$log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true));
|
||||
$log->content([
|
||||
'time' => time(),
|
||||
'location' => $destination
|
||||
]);
|
||||
$log->save();
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function convertExclude($exclude)
|
||||
{
|
||||
$lines = preg_split("/[\s,]+/", $exclude);
|
||||
return array_map('trim', $lines, array_fill(0, \count($lines), '/'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Internally uses the PhpUserAgent package https://github.com/donatj/PhpUserAgent
|
||||
*/
|
||||
class Browser
|
||||
{
|
||||
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 string 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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use \Doctrine\Common\Cache as DoctrineCache;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Scheduler\Scheduler;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\Event\EventDispatcher;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
protected $lifetime;
|
||||
protected $now;
|
||||
|
||||
/** @var Config $config */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var DoctrineCache\CacheProvider
|
||||
*/
|
||||
protected $driver;
|
||||
|
||||
/**
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $simpleCache;
|
||||
|
||||
protected $driver_name;
|
||||
|
||||
protected $driver_setting;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled;
|
||||
|
||||
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
|
||||
*/
|
||||
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);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
$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 object|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 $data the data for the cached entry to store
|
||||
* @param int $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
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter method to set key (Advanced)
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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 mixed
|
||||
*/
|
||||
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 mixed
|
||||
*/
|
||||
public function getDriverName()
|
||||
{
|
||||
return $this->driver_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current driver setting
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public static function purgeJob()
|
||||
{
|
||||
/** @var Cache $cache */
|
||||
$cache = Grav::instance()['cache'];
|
||||
$deleted_folders = $cache->purgeOldCache();
|
||||
|
||||
echo 'Purged ' . $deleted_folders . ' old cache folders...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to call as a scheduled Job to clear Grav cache
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public static function clearJob($type)
|
||||
{
|
||||
$result = static::clearCache($type);
|
||||
static::invalidateCache();
|
||||
|
||||
echo strip_tags(implode("\n", $result));
|
||||
}
|
||||
|
||||
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', [], $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');
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace 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(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,263 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
|
||||
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 string Timestamp of compiled configuration
|
||||
*/
|
||||
public $timestamp;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
$this->timestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename for the compiled PHP file.
|
||||
*
|
||||
* @param string $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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
protected function createFilename()
|
||||
{
|
||||
return "{$this->cacheFolder}/{$this->name()->name}.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
abstract protected function createObject(array $data = []);
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*/
|
||||
abstract protected function finalizeObject();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* @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();
|
||||
}
|
||||
|
||||
protected function getState()
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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
|
||||
{
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
protected function getState()
|
||||
{
|
||||
return $this->object->getState();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
|
||||
class CompiledConfig extends CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var callable Blueprints loader.
|
||||
*/
|
||||
protected $callable;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $withDefaults;
|
||||
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
protected function finalizeObject()
|
||||
{
|
||||
$this->object->checksum($this->checksum());
|
||||
$this->object->timestamp($this->timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
protected function loadFile($name, $filename)
|
||||
{
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$this->object->join($name, $file->content(), '/');
|
||||
$file->free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends CompiledBase
|
||||
{
|
||||
public function __construct($cacheFolder, array $files, $path)
|
||||
{
|
||||
parent::__construct($cacheFolder, $files, $path);
|
||||
|
||||
$this->version = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
protected function createObject(array $data = [])
|
||||
{
|
||||
$this->object = new Languages($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize configuration object.
|
||||
*/
|
||||
protected function finalizeObject()
|
||||
{
|
||||
$this->object->checksum($this->checksum());
|
||||
$this->object->timestamp($this->timestamp());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
class Config extends Data
|
||||
{
|
||||
public $environment;
|
||||
|
||||
/** @var string */
|
||||
protected $key;
|
||||
/** @var string */
|
||||
protected $checksum;
|
||||
/** @var int */
|
||||
protected $timestamp = 0;
|
||||
/** @var bool */
|
||||
protected $modified = false;
|
||||
|
||||
public function key()
|
||||
{
|
||||
if (null === $this->key) {
|
||||
$this->key = md5($this->checksum . $this->timestamp);
|
||||
}
|
||||
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function checksum($checksum = null)
|
||||
{
|
||||
if ($checksum !== null) {
|
||||
$this->checksum = $checksum;
|
||||
}
|
||||
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
public function modified($modified = null)
|
||||
{
|
||||
if ($modified !== null) {
|
||||
$this->modified = $modified;
|
||||
}
|
||||
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
public function timestamp($timestamp = null)
|
||||
{
|
||||
if ($timestamp !== null) {
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
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,263 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
|
||||
class ConfigFileFinder
|
||||
{
|
||||
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 $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 $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);
|
||||
|
||||
/** @var \DirectoryIterator $directory */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends Data
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $checksum;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $modified;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $timestamp;
|
||||
|
||||
|
||||
public function checksum($checksum = null)
|
||||
{
|
||||
if ($checksum !== null) {
|
||||
$this->checksum = $checksum;
|
||||
}
|
||||
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
public function modified($modified = null)
|
||||
{
|
||||
if ($modified !== null) {
|
||||
$this->modified = $modified;
|
||||
}
|
||||
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
public function timestamp($timestamp = null)
|
||||
{
|
||||
if ($timestamp !== null) {
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function reformat()
|
||||
{
|
||||
if (isset($this->items['plugins'])) {
|
||||
$this->items = array_merge_recursive($this->items, $this->items['plugins']);
|
||||
unset($this->items['plugins']);
|
||||
}
|
||||
}
|
||||
|
||||
public function mergeRecursive(array $data)
|
||||
{
|
||||
$this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
|
||||
}
|
||||
|
||||
public function flattenByLang($lang)
|
||||
{
|
||||
$language = $this->items[$lang];
|
||||
return Utils::arrayFlattenDotNotation($language);
|
||||
}
|
||||
|
||||
public function unflatten($array)
|
||||
{
|
||||
return Utils::arrayUnflattenDotNotation($array);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Config
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Utils;
|
||||
use Pimple\Container;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
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 Current environment normalized to lower case.
|
||||
*/
|
||||
public static $environment;
|
||||
|
||||
protected $streams = [
|
||||
'system' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['system'],
|
||||
]
|
||||
],
|
||||
'user' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['user'],
|
||||
]
|
||||
],
|
||||
'environment' => [
|
||||
'type' => 'ReadOnlyStream'
|
||||
// If not defined, environment will be set up in the constructor.
|
||||
],
|
||||
'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'],
|
||||
]
|
||||
],
|
||||
'cache' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['cache'],
|
||||
'images' => ['images']
|
||||
]
|
||||
],
|
||||
'log' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['logs']
|
||||
]
|
||||
],
|
||||
'backup' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['backup']
|
||||
]
|
||||
],
|
||||
'tmp' => [
|
||||
'type' => 'Stream',
|
||||
'force' => true,
|
||||
'prefixes' => [
|
||||
'' => ['tmp']
|
||||
]
|
||||
],
|
||||
'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)
|
||||
{
|
||||
// If no environment is set, make sure we get one (CLI or hostname).
|
||||
if (!static::$environment) {
|
||||
if (\defined('GRAV_CLI')) {
|
||||
static::$environment = 'cli';
|
||||
} else {
|
||||
/** @var ServerRequestInterface $request */
|
||||
$request = $container['request'];
|
||||
$host = $request->getUri()->getHost();
|
||||
|
||||
static::$environment = Utils::substrToString($host, ':');
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve server aliases to the proper environment.
|
||||
$environment = $this->environments[static::$environment] ?? static::$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.
|
||||
$file = \defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php';
|
||||
$setup = is_file($file) ? (array) include $file : [];
|
||||
|
||||
// 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);
|
||||
|
||||
// Set up environment.
|
||||
$this->def('environment', $environment);
|
||||
$this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->get('environment')}"]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @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
|
||||
* @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 (!$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
class Blueprint extends BlueprintForm
|
||||
{
|
||||
/** @var string */
|
||||
protected $context = 'blueprints://';
|
||||
|
||||
protected $scope;
|
||||
|
||||
/** @var BlueprintSchema */
|
||||
protected $blueprintSchema;
|
||||
|
||||
/** @var array */
|
||||
protected $defaults;
|
||||
|
||||
protected $handlers = [];
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->blueprintSchema) {
|
||||
$this->blueprintSchema = clone $this->blueprintSchema;
|
||||
}
|
||||
}
|
||||
|
||||
public function setScope($scope)
|
||||
{
|
||||
$this->scope = $scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values for field types.
|
||||
*
|
||||
* @param array $types
|
||||
* @return $this
|
||||
*/
|
||||
public function setTypes(array $types)
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
$this->blueprintSchema->setTypes($types);
|
||||
|
||||
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()
|
||||
{
|
||||
$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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two arrays by using blueprints.
|
||||
*
|
||||
* @param array $data1
|
||||
* @param array $data2
|
||||
* @param string $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
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
$this->blueprintSchema->validate($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array
|
||||
*/
|
||||
public function flattenData(array $data)
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema->flattenData($data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return blueprint data schema.
|
||||
*
|
||||
* @return BlueprintSchema
|
||||
*/
|
||||
public function schema()
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
return $this->blueprintSchema;
|
||||
}
|
||||
|
||||
public function addDynamicHandler(string $name, callable $callable): void
|
||||
{
|
||||
$this->handlers[$name] = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize validator.
|
||||
*/
|
||||
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 string
|
||||
*/
|
||||
protected function loadFile($filename)
|
||||
{
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$content = $file->content();
|
||||
$file->free();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $path
|
||||
* @param string $context
|
||||
* @return array
|
||||
*/
|
||||
protected function getFiles($path, $context = null)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if (\is_string($path) && !$locator->isStream($path)) {
|
||||
// Find path overrides.
|
||||
$paths = (array) ($this->overrides[$path] ?? null);
|
||||
|
||||
// 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
*/
|
||||
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;
|
||||
foreach ($actions as $action) {
|
||||
if (!$user || !$user->authorize($action)) {
|
||||
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $field
|
||||
* @param string $property
|
||||
* @param array $call
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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,319 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\Blueprints\BlueprintSchema as BlueprintSchemaBase;
|
||||
|
||||
class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
{
|
||||
use Export;
|
||||
|
||||
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
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
{
|
||||
try {
|
||||
$messages = $this->validateArray($data, $this->nested);
|
||||
|
||||
} 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)
|
||||
{
|
||||
return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten data by using blueprints.
|
||||
*
|
||||
* @param array $data Data to be flattened.
|
||||
* @return array
|
||||
*/
|
||||
public function flattenData(array $data)
|
||||
{
|
||||
return $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
|
||||
* @return array
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateArray(array $data, array $rules)
|
||||
{
|
||||
$messages = $this->checkRequired($data, $rules);
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = \is_string($val) ? $this->items[$val] : null;
|
||||
|
||||
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($field, $rule);
|
||||
} elseif (\is_array($field) && \is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$messages += $this->validateArray($field, $val);
|
||||
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
|
||||
// Undefined/extra item.
|
||||
throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @param bool $missingValuesAsNull
|
||||
* @param bool $keepEmptyValues
|
||||
* @return array
|
||||
*/
|
||||
protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
if ($missingValuesAsNull) {
|
||||
// First pass is to fill up all the fields with null. This is done to lock the ordering of the fields.
|
||||
foreach ($rules as $key => $rule) {
|
||||
if ($key && !isset($results[$key])) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = \is_string($val) ? $this->items[$val] : null;
|
||||
|
||||
if (empty($rule['disabled']) && empty($rule['validate']['ignore'])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = \is_string($val) ? $this->items[$val] : null;
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
|
||||
// Skip any data in the ignored field.
|
||||
unset($results[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$field = Validation::filter($field, $rule);
|
||||
} elseif (\is_array($field) && \is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues);
|
||||
|
||||
} 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|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)) {
|
||||
// Recursively fetch the items.
|
||||
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggles[$key] ?? [], $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 toggleable and the toggle is turned off
|
||||
|| (!empty($field['toggleable']) && 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** @var \DirectoryIterator $file */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use RocketTheme\Toolbox\ArrayTraits\Countable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
|
||||
class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable, ExportInterface
|
||||
{
|
||||
use NestedArrayAccessWithGetters, Countable, Export;
|
||||
|
||||
/** @var string */
|
||||
protected $gettersVariable = 'items';
|
||||
|
||||
/** @var array */
|
||||
protected $items;
|
||||
|
||||
/** @var Blueprint */
|
||||
protected $blueprints;
|
||||
|
||||
/** @var File */
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* @param array $items
|
||||
* @param Blueprint|callable $blueprints
|
||||
*/
|
||||
public function __construct(array $items = [], $blueprints = null)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->blueprints = $blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) ?: false);
|
||||
$keepEmptyValues = (bool)(array_shift($args) ?: false);
|
||||
|
||||
$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.
|
||||
* @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 $storage Optionally enter a new storage.
|
||||
* @return FileInterface
|
||||
*/
|
||||
public function file(FileInterface $storage = null)
|
||||
{
|
||||
if ($storage) {
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
|
||||
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.
|
||||
*/
|
||||
public function blueprints();
|
||||
|
||||
/**
|
||||
* Validate by blueprints.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function validate();
|
||||
|
||||
/**
|
||||
* Filter all items by using blueprints.
|
||||
*/
|
||||
public function filter();
|
||||
|
||||
/**
|
||||
* Get extra items which haven't been defined in blueprints.
|
||||
*/
|
||||
public function extra();
|
||||
|
||||
/**
|
||||
* Save data into the file.
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Set or get the data storage.
|
||||
*
|
||||
* @param FileInterface $storage Optionally enter a new storage.
|
||||
* @return FileInterface
|
||||
*/
|
||||
public function file(FileInterface $storage = null);
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Yaml;
|
||||
|
||||
class Validation
|
||||
{
|
||||
/**
|
||||
* Validate value against a blueprint field definition.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
public static function validate($value, array $field)
|
||||
{
|
||||
if (!isset($field['type'])) {
|
||||
$field['type'] = 'text';
|
||||
}
|
||||
|
||||
$validate = (array)($field['validate'] ?? null);
|
||||
$type = $validate['type'] ?? $field['type'];
|
||||
$required = $validate['required'] ?? false;
|
||||
|
||||
// If value isn't required, we will stop validation if empty value is given.
|
||||
if ($required !== true && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get language class.
|
||||
$language = Grav::instance()['language'];
|
||||
|
||||
$name = ucfirst($field['label'] ?? $field['name']);
|
||||
$message = (string) isset($field['validate']['message'])
|
||||
? $language->translate($field['validate']['message'])
|
||||
: $language->translate('GRAV.FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
|
||||
|
||||
|
||||
// Validate type with fallback type text.
|
||||
$method = 'type' . str_replace('-', '_', $type);
|
||||
|
||||
// If this is a YAML field validate/filter as such
|
||||
if (isset($field['yaml']) && $field['yaml'] === true) {
|
||||
$method = 'typeYaml';
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
|
||||
$success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true;
|
||||
if (!$success) {
|
||||
$messages[$field['name']][] = $message;
|
||||
}
|
||||
|
||||
// Check individual rules.
|
||||
foreach ($validate as $rule => $params) {
|
||||
$method = 'validate' . ucfirst(str_replace('-', '_', $rule));
|
||||
|
||||
if (method_exists(__CLASS__, $method)) {
|
||||
$success = self::$method($value, $params);
|
||||
|
||||
if (!$success) {
|
||||
$messages[$field['name']][] = $message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter value against a blueprint field definition.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @return mixed Filtered value.
|
||||
*/
|
||||
public static function filter($value, array $field)
|
||||
{
|
||||
$validate = (array)($field['filter'] ?? $field['validate'] ?? null);
|
||||
|
||||
// If value isn't required, we will return null if empty value is given.
|
||||
if (($value === null || $value === '') && empty($validate['required'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($field['type'])) {
|
||||
$field['type'] = 'text';
|
||||
}
|
||||
$type = $field['filter']['type'] ?? $field['validate']['type'] ?? $field['type'];
|
||||
|
||||
$method = 'filter' . ucfirst(str_replace('-', '_', $type));
|
||||
|
||||
// If this is a YAML field validate/filter as such
|
||||
if (isset($field['yaml']) && $field['yaml'] === true) {
|
||||
$method = 'filterYaml';
|
||||
}
|
||||
|
||||
if (!method_exists(__CLASS__, $method)) {
|
||||
$method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText';
|
||||
}
|
||||
|
||||
return self::$method($value, $validate, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: text
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeText($value, array $params, array $field)
|
||||
{
|
||||
if (!\is_string($value) && !is_numeric($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = (string)$value;
|
||||
|
||||
if (!empty($params['trim'])) {
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
if (isset($params['min']) && \strlen($value) < $params['min']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($params['max']) && \strlen($value) > $params['max']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$min = $params['min'] ?? 0;
|
||||
if (isset($params['step']) && (\strlen($value) - $min) % $params['step'] === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function filterText($value, array $params, array $field)
|
||||
{
|
||||
if (!\is_string($value) && !is_numeric($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!empty($params['trim'])) {
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
protected static function filterCheckbox($value, array $params, array $field)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
protected static function filterCommaList($value, array $params, array $field)
|
||||
{
|
||||
return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
public static function typeCommaList($value, array $params, array $field)
|
||||
{
|
||||
return \is_array($value) ? true : self::typeText($value, $params, $field);
|
||||
}
|
||||
|
||||
protected static function filterLower($value, array $params)
|
||||
{
|
||||
return strtolower($value);
|
||||
}
|
||||
|
||||
protected static function filterUpper($value, array $params)
|
||||
{
|
||||
return strtoupper($value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* HTML5 input: textarea
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeTextarea($value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['multiline'])) {
|
||||
$params['multiline'] = true;
|
||||
}
|
||||
|
||||
return self::typeText($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: password
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typePassword($value, array $params, array $field)
|
||||
{
|
||||
return self::typeText($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: hidden
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeHidden($value, array $params, array $field)
|
||||
{
|
||||
return self::typeText($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom input: checkbox list
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeCheckboxes($value, array $params, array $field)
|
||||
{
|
||||
// Set multiple: true so checkboxes can easily use min/max counts to control number of options required
|
||||
$field['multiple'] = true;
|
||||
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
|
||||
protected static function filterCheckboxes($value, array $params, array $field)
|
||||
{
|
||||
return self::filterArray($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: checkbox
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeCheckbox($value, array $params, array $field)
|
||||
{
|
||||
$value = (string)$value;
|
||||
$field_value = (string)($field['value'] ?? '1');
|
||||
|
||||
return $value === $field_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: radio
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeRadio($value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom input: toggle
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeToggle($value, array $params, array $field)
|
||||
{
|
||||
if (\is_bool($value)) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom input: file
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeFile($value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array)$value, $params, $field);
|
||||
}
|
||||
|
||||
protected static function filterFile($value, array $params, array $field)
|
||||
{
|
||||
return (array)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: select
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeSelect($value, array $params, array $field)
|
||||
{
|
||||
return self::typeArray((array) $value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: number
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeNumber($value, array $params, array $field)
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($params['min']) && $value < $params['min']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($params['max']) && $value > $params['max']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$min = $params['min'] ?? 0;
|
||||
|
||||
return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
|
||||
}
|
||||
|
||||
protected static function filterNumber($value, array $params, array $field)
|
||||
{
|
||||
return (string)(int)$value !== (string)(float)$value ? (float) $value : (int) $value;
|
||||
}
|
||||
|
||||
protected static function filterDateTime($value, array $params, array $field)
|
||||
{
|
||||
$format = Grav::instance()['config']->get('system.pages.dateformat.default');
|
||||
if ($format) {
|
||||
$converted = new \DateTime($value);
|
||||
return $converted->format($format);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* HTML5 input: range
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeRange($value, array $params, array $field)
|
||||
{
|
||||
return self::typeNumber($value, $params, $field);
|
||||
}
|
||||
|
||||
protected static function filterRange($value, array $params, array $field)
|
||||
{
|
||||
return self::filterNumber($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: color
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeColor($value, array $params, array $field)
|
||||
{
|
||||
return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: email
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeEmail($value, array $params, array $field)
|
||||
{
|
||||
$values = !\is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
|
||||
|
||||
foreach ($values as $val) {
|
||||
if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: url
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
|
||||
public static function typeUrl($value, array $params, array $field)
|
||||
{
|
||||
return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: datetime
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDatetime($value, array $params, array $field)
|
||||
{
|
||||
if ($value instanceof \DateTime) {
|
||||
return true;
|
||||
}
|
||||
if (!\is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($params['format'])) {
|
||||
return false !== strtotime($value);
|
||||
}
|
||||
|
||||
$dateFromFormat = \DateTime::createFromFormat($params['format'], $value);
|
||||
|
||||
return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: datetime-local
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDatetimeLocal($value, array $params, array $field)
|
||||
{
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: date
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeDate($value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'Y-m-d';
|
||||
}
|
||||
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: time
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeTime($value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'H:i';
|
||||
}
|
||||
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: month
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeMonth($value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format'])) {
|
||||
$params['format'] = 'Y-m';
|
||||
}
|
||||
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 input: week
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeWeek($value, array $params, array $field)
|
||||
{
|
||||
if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::typeDatetime($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom input: array
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeArray($value, array $params, array $field)
|
||||
{
|
||||
if (!\is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($field['multiple'])) {
|
||||
if (isset($params['min']) && \count($value) < $params['min']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($params['max']) && \count($value) > $params['max']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$min = $params['min'] ?? 0;
|
||||
if (isset($params['step']) && (\count($value) - $min) % $params['step'] === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If creating new values is allowed, no further checks are needed.
|
||||
if (!empty($field['selectize']['create'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$options = $field['options'] ?? [];
|
||||
$use = $field['use'] ?? 'values';
|
||||
|
||||
if (empty($field['selectize']) || empty($field['multiple'])) {
|
||||
$options = array_keys($options);
|
||||
}
|
||||
if ($use === 'keys') {
|
||||
$value = array_keys($value);
|
||||
}
|
||||
|
||||
return !($options && array_diff($value, $options));
|
||||
}
|
||||
|
||||
protected static function filterArray($value, $params, $field)
|
||||
{
|
||||
$values = (array) $value;
|
||||
$options = isset($field['options']) ? array_keys($field['options']) : [];
|
||||
$multi = $field['multiple'] ?? false;
|
||||
|
||||
if (\count($values) === 1 && isset($values[0]) && $values[0] === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if ($options) {
|
||||
$useKey = isset($field['use']) && $field['use'] === 'keys';
|
||||
foreach ($values as $key => $val) {
|
||||
$values[$key] = $useKey ? (bool) $val : $val;
|
||||
}
|
||||
}
|
||||
|
||||
if ($multi) {
|
||||
foreach ($values as $key => $val) {
|
||||
if (\is_array($val)) {
|
||||
$val = implode(',', $val);
|
||||
$values[$key] = array_map('trim', explode(',', $val));
|
||||
} else {
|
||||
$values[$key] = trim($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) {
|
||||
foreach ($values as $key => $val) {
|
||||
if ($val === '') {
|
||||
unset($values[$key]);
|
||||
} elseif (\is_array($val)) {
|
||||
foreach ($val as $inner_key => $inner_value) {
|
||||
if ($inner_value === '') {
|
||||
unset($val[$inner_key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$values[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public static function typeList($value, array $params, array $field)
|
||||
{
|
||||
if (!\is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($field['fields'])) {
|
||||
foreach ($value as $key => $item) {
|
||||
foreach ($field['fields'] as $subKey => $subField) {
|
||||
$subKey = trim($subKey, '.');
|
||||
$subValue = $item[$subKey] ?? null;
|
||||
self::validate($subValue, $subField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function filterList($value, array $params, array $field)
|
||||
{
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
public static function filterYaml($value, $params)
|
||||
{
|
||||
if (!\is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return (array) Yaml::parse($value);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom input: ignore (will not validate)
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeIgnore($value, array $params, array $field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function filterIgnore($value, array $params, array $field)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input value which can be ignored.
|
||||
*
|
||||
* @param mixed $value Value to be validated.
|
||||
* @param array $params Validation parameters.
|
||||
* @param array $field Blueprint for the field.
|
||||
* @return bool True if validation succeeded.
|
||||
*/
|
||||
public static function typeUnset($value, array $params, array $field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function filterUnset($value, array $params, array $field)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// HTML5 attributes (min, max and range are handled inside the types)
|
||||
|
||||
public static function validateRequired($value, $params)
|
||||
{
|
||||
if (is_scalar($value)) {
|
||||
return (bool) $params !== true || $value !== '';
|
||||
}
|
||||
|
||||
return (bool) $params !== true || !empty($value);
|
||||
}
|
||||
|
||||
public static function validatePattern($value, $params)
|
||||
{
|
||||
return (bool) preg_match("`^{$params}$`u", $value);
|
||||
}
|
||||
|
||||
|
||||
// Internal types
|
||||
|
||||
public static function validateAlpha($value, $params)
|
||||
{
|
||||
return ctype_alpha($value);
|
||||
}
|
||||
|
||||
public static function validateAlnum($value, $params)
|
||||
{
|
||||
return ctype_alnum($value);
|
||||
}
|
||||
|
||||
public static function typeBool($value, $params)
|
||||
{
|
||||
return \is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
public static function validateBool($value, $params)
|
||||
{
|
||||
return \is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
protected static function filterBool($value, $params)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
public static function validateDigit($value, $params)
|
||||
{
|
||||
return ctype_digit($value);
|
||||
}
|
||||
|
||||
public static function validateFloat($value, $params)
|
||||
{
|
||||
return \is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
|
||||
}
|
||||
|
||||
protected static function filterFloat($value, $params)
|
||||
{
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
public static function validateHex($value, $params)
|
||||
{
|
||||
return ctype_xdigit($value);
|
||||
}
|
||||
|
||||
public static function validateInt($value, $params)
|
||||
{
|
||||
return is_numeric($value) && (int)$value == $value;
|
||||
}
|
||||
|
||||
protected static function filterInt($value, $params)
|
||||
{
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
public static function validateArray($value, $params)
|
||||
{
|
||||
return \is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable && $value instanceof \Countable);
|
||||
}
|
||||
|
||||
public static function filterItem_List($value, $params)
|
||||
{
|
||||
return array_values(array_filter($value, function($v) { return !empty($v); } ));
|
||||
}
|
||||
|
||||
public static function validateJson($value, $params)
|
||||
{
|
||||
return (bool) (@json_decode($value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Data
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
class ValidationException extends \RuntimeException
|
||||
{
|
||||
protected $messages = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getMessages()
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use DebugBar\DataCollector\ConfigCollector;
|
||||
use DebugBar\DataCollector\DataCollectorInterface;
|
||||
use DebugBar\DataCollector\ExceptionsCollector;
|
||||
use DebugBar\DataCollector\MemoryCollector;
|
||||
use DebugBar\DataCollector\MessagesCollector;
|
||||
use DebugBar\DataCollector\PhpInfoCollector;
|
||||
use DebugBar\DataCollector\RequestDataCollector;
|
||||
use DebugBar\DataCollector\TimeDataCollector;
|
||||
use DebugBar\DebugBar;
|
||||
use DebugBar\JavascriptRenderer;
|
||||
use DebugBar\StandardDebugBar;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Processors\ProcessorInterface;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
class Debugger
|
||||
{
|
||||
/** @var Grav $grav */
|
||||
protected $grav;
|
||||
|
||||
/** @var Config $config */
|
||||
protected $config;
|
||||
|
||||
/** @var JavascriptRenderer $renderer */
|
||||
protected $renderer;
|
||||
|
||||
/** @var StandardDebugBar $debugbar */
|
||||
protected $debugbar;
|
||||
|
||||
/** @var bool */
|
||||
protected $enabled;
|
||||
|
||||
protected $initialized = false;
|
||||
|
||||
/** @var array */
|
||||
protected $timers = [];
|
||||
|
||||
/** @var array $deprecations */
|
||||
protected $deprecations = [];
|
||||
|
||||
/** @var callable */
|
||||
protected $errorHandler;
|
||||
|
||||
/**
|
||||
* Debugger constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$currentTime = microtime(true);
|
||||
|
||||
if (!\defined('GRAV_REQUEST_TIME')) {
|
||||
\define('GRAV_REQUEST_TIME', $currentTime);
|
||||
}
|
||||
|
||||
// Enable debugger until $this->init() gets called.
|
||||
$this->enabled = true;
|
||||
|
||||
$debugbar = new DebugBar();
|
||||
$debugbar->addCollector(new PhpInfoCollector());
|
||||
$debugbar->addCollector(new MessagesCollector());
|
||||
$debugbar->addCollector(new RequestDataCollector());
|
||||
$debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME));
|
||||
|
||||
$debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
|
||||
$debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime);
|
||||
$debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true));
|
||||
|
||||
$this->debugbar = $debugbar;
|
||||
|
||||
// Set deprecation collector.
|
||||
$this->setErrorHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the debugger
|
||||
*
|
||||
* @return $this
|
||||
* @throws \DebugBar\DebugBarException
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
if ($this->initialized) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->grav = Grav::instance();
|
||||
$this->config = $this->grav['config'];
|
||||
|
||||
// Enable/disable debugger based on configuration.
|
||||
$this->enabled = (bool)$this->config->get('system.debugger.enabled');
|
||||
|
||||
if ($this->enabled()) {
|
||||
$this->initialized = true;
|
||||
|
||||
$plugins_config = (array)$this->config->get('plugins');
|
||||
|
||||
ksort($plugins_config);
|
||||
|
||||
$debugbar = $this->debugbar;
|
||||
$debugbar->addCollector(new MemoryCollector());
|
||||
$debugbar->addCollector(new ExceptionsCollector());
|
||||
$debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
|
||||
$debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
|
||||
$this->addMessage('Grav v' . GRAV_VERSION);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set/get the enabled state of the debugger
|
||||
*
|
||||
* @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function enabled($state = null)
|
||||
{
|
||||
if ($state !== null) {
|
||||
$this->enabled = (bool)$state;
|
||||
}
|
||||
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the debugger assets to the Grav Assets
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addAssets()
|
||||
{
|
||||
if ($this->enabled()) {
|
||||
|
||||
// Only add assets if Page is HTML
|
||||
$page = $this->grav['page'];
|
||||
if ($page->templateFormat() !== 'html') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @var Assets $assets */
|
||||
$assets = $this->grav['assets'];
|
||||
|
||||
// Add jquery library
|
||||
$assets->add('jquery', 101);
|
||||
|
||||
$this->renderer = $this->debugbar->getJavascriptRenderer();
|
||||
$this->renderer->setIncludeVendors(false);
|
||||
|
||||
// Get the required CSS files
|
||||
list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
|
||||
foreach ((array)$css_files as $css) {
|
||||
$assets->addCss($css);
|
||||
}
|
||||
|
||||
$assets->addCss('/system/assets/debugger.css');
|
||||
|
||||
foreach ((array)$js_files as $js) {
|
||||
$assets->addJs($js);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCaller($limit = 2)
|
||||
{
|
||||
$trace = debug_backtrace(false, $limit);
|
||||
|
||||
return array_pop($trace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data collector
|
||||
*
|
||||
* @param DataCollectorInterface $collector
|
||||
*
|
||||
* @return $this
|
||||
* @throws \DebugBar\DebugBarException
|
||||
*/
|
||||
public function addCollector($collector)
|
||||
{
|
||||
$this->debugbar->addCollector($collector);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a data collector
|
||||
*
|
||||
* @param DataCollectorInterface $collector
|
||||
*
|
||||
* @return DataCollectorInterface
|
||||
* @throws \DebugBar\DebugBarException
|
||||
*/
|
||||
public function getCollector($collector)
|
||||
{
|
||||
return $this->debugbar->getCollector($collector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the debug bar
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if ($this->enabled()) {
|
||||
// Only add assets if Page is HTML
|
||||
$page = $this->grav['page'];
|
||||
if (!$this->renderer || $page->templateFormat() !== 'html') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->addDeprecations();
|
||||
|
||||
echo $this->renderer->render();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data through the HTTP headers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendDataInHeaders()
|
||||
{
|
||||
if ($this->enabled()) {
|
||||
$this->addDeprecations();
|
||||
$this->debugbar->sendDataInHeaders();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns collected debugger data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->addDeprecations();
|
||||
$this->timers = [];
|
||||
|
||||
return $this->debugbar->getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a timer with an associated name and description
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $description
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function startTimer($name, $description = null)
|
||||
{
|
||||
if (strpos($name, '_') === 0 || $this->enabled()) {
|
||||
$this->debugbar['time']->startMeasure($name, $description);
|
||||
$this->timers[] = $name;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the named timer
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function stopTimer($name)
|
||||
{
|
||||
if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) {
|
||||
$this->debugbar['time']->stopMeasure($name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump variables into the Messages tab of the Debug Bar
|
||||
*
|
||||
* @param mixed $message
|
||||
* @param string $label
|
||||
* @param bool $isString
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addMessage($message, $label = 'info', $isString = true)
|
||||
{
|
||||
if ($this->enabled()) {
|
||||
$this->debugbar['messages']->addMessage($message, $label, $isString);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump exception into the Messages tab of the Debug Bar
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return Debugger
|
||||
*/
|
||||
public function addException(\Exception $e)
|
||||
{
|
||||
if ($this->initialized && $this->enabled()) {
|
||||
$this->debugbar['exceptions']->addException($e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setErrorHandler()
|
||||
{
|
||||
$this->errorHandler = set_error_handler(
|
||||
[$this, 'deprecatedErrorHandler']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
* @return bool
|
||||
*/
|
||||
public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
|
||||
{
|
||||
if ($errno !== E_USER_DEPRECATED) {
|
||||
if ($this->errorHandler) {
|
||||
return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->enabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Figure out error scope from the error.
|
||||
$scope = 'unknown';
|
||||
if (stripos($errstr, 'grav') !== false) {
|
||||
$scope = 'grav';
|
||||
} elseif (strpos($errfile, '/twig/') !== false) {
|
||||
$scope = 'twig';
|
||||
} elseif (stripos($errfile, '/yaml/') !== false) {
|
||||
$scope = 'yaml';
|
||||
} elseif (strpos($errfile, '/vendor/') !== false) {
|
||||
$scope = 'vendor';
|
||||
}
|
||||
|
||||
// Clean up backtrace to make it more useful.
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
|
||||
|
||||
// Skip current call.
|
||||
array_shift($backtrace);
|
||||
|
||||
// Find yaml file where the error happened.
|
||||
if ($scope === 'yaml') {
|
||||
foreach ($backtrace as $current) {
|
||||
if (isset($current['args'])) {
|
||||
foreach ($current['args'] as $arg) {
|
||||
if ($arg instanceof \SplFileInfo) {
|
||||
$arg = $arg->getPathname();
|
||||
}
|
||||
if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
|
||||
$errfile = $arg;
|
||||
$errline = 0;
|
||||
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter arguments.
|
||||
$cut = 0;
|
||||
$previous = null;
|
||||
foreach ($backtrace as $i => &$current) {
|
||||
if (isset($current['args'])) {
|
||||
$args = [];
|
||||
foreach ($current['args'] as $arg) {
|
||||
if (\is_string($arg)) {
|
||||
$arg = "'" . $arg . "'";
|
||||
if (mb_strlen($arg) > 100) {
|
||||
$arg = 'string';
|
||||
}
|
||||
} elseif (\is_bool($arg)) {
|
||||
$arg = $arg ? 'true' : 'false';
|
||||
} elseif (\is_scalar($arg)) {
|
||||
$arg = $arg;
|
||||
} elseif (\is_object($arg)) {
|
||||
$arg = get_class($arg) . ' $object';
|
||||
} elseif (\is_array($arg)) {
|
||||
$arg = '$array';
|
||||
} else {
|
||||
$arg = '$object';
|
||||
}
|
||||
|
||||
$args[] = $arg;
|
||||
}
|
||||
$current['args'] = $args;
|
||||
}
|
||||
|
||||
$object = $current['object'] ?? null;
|
||||
unset($current['object']);
|
||||
|
||||
$reflection = null;
|
||||
if ($object instanceof TemplateWrapper) {
|
||||
$reflection = new \ReflectionObject($object);
|
||||
$property = $reflection->getProperty('template');
|
||||
$property->setAccessible(true);
|
||||
$object = $property->getValue($object);
|
||||
}
|
||||
|
||||
if ($object instanceof Template) {
|
||||
$file = $current['file'] ?? null;
|
||||
|
||||
if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
|
||||
$current = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$debugInfo = $object->getDebugInfo();
|
||||
|
||||
$line = 1;
|
||||
if (!$reflection) {
|
||||
foreach ($debugInfo as $codeLine => $templateLine) {
|
||||
if ($codeLine <= $current['line']) {
|
||||
$line = $templateLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$src = $object->getSourceContext();
|
||||
//$code = preg_split('/\r\n|\r|\n/', $src->getCode());
|
||||
//$current['twig']['twig'] = trim($code[$line - 1]);
|
||||
$current['twig']['file'] = $src->getPath();
|
||||
$current['twig']['line'] = $line;
|
||||
|
||||
$prevFile = $previous['file'] ?? null;
|
||||
if ($prevFile && $file === $prevFile) {
|
||||
$prevLine = $previous['line'];
|
||||
|
||||
$line = 1;
|
||||
foreach ($debugInfo as $codeLine => $templateLine) {
|
||||
if ($codeLine <= $prevLine) {
|
||||
$line = $templateLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//$previous['twig']['twig'] = trim($code[$line - 1]);
|
||||
$previous['twig']['file'] = $src->getPath();
|
||||
$previous['twig']['line'] = $line;
|
||||
}
|
||||
|
||||
$cut = $i;
|
||||
} elseif ($object instanceof ProcessorInterface) {
|
||||
$cut = $cut ?: $i;
|
||||
break;
|
||||
}
|
||||
|
||||
$previous = &$backtrace[$i];
|
||||
}
|
||||
unset($current);
|
||||
|
||||
if ($cut) {
|
||||
$backtrace = array_slice($backtrace, 0, $cut + 1);
|
||||
}
|
||||
$backtrace = array_values(array_filter($backtrace));
|
||||
|
||||
// Skip vendor libraries and the method where error was triggered.
|
||||
foreach ($backtrace as $i => $current) {
|
||||
if (!isset($current['file'])) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($current['file'], '/vendor/') !== false) {
|
||||
$cut = $i + 1;
|
||||
continue;
|
||||
}
|
||||
if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
|
||||
$cut = $i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($cut) {
|
||||
$backtrace = array_slice($backtrace, $cut);
|
||||
}
|
||||
$backtrace = array_values(array_filter($backtrace));
|
||||
|
||||
$current = reset($backtrace);
|
||||
|
||||
// If the issue happened inside twig file, change the file and line to match that file.
|
||||
$file = $current['twig']['file'] ?? '';
|
||||
if ($file) {
|
||||
$errfile = $file;
|
||||
$errline = $current['twig']['line'] ?? 0;
|
||||
}
|
||||
|
||||
$deprecation = [
|
||||
'scope' => $scope,
|
||||
'message' => $errstr,
|
||||
'file' => $errfile,
|
||||
'line' => $errline,
|
||||
'trace' => $backtrace,
|
||||
'count' => 1
|
||||
];
|
||||
|
||||
$this->deprecations[] = $deprecation;
|
||||
|
||||
// Do not pass forward.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function addDeprecations()
|
||||
{
|
||||
if (!$this->deprecations) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collector = new MessagesCollector('deprecated');
|
||||
$this->addCollector($collector);
|
||||
$collector->addMessage('Your site is using following deprecated features:');
|
||||
|
||||
/** @var array $deprecated */
|
||||
foreach ($this->deprecations as $deprecated) {
|
||||
list($message, $scope) = $this->getDepracatedMessage($deprecated);
|
||||
|
||||
$collector->addMessage($message, $scope);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDepracatedMessage($deprecated)
|
||||
{
|
||||
$scope = $deprecated['scope'];
|
||||
|
||||
$trace = [];
|
||||
if (isset($deprecated['trace'])) {
|
||||
foreach ($deprecated['trace'] as $current) {
|
||||
$class = $current['class'] ?? '';
|
||||
$type = $current['type'] ?? '';
|
||||
$function = $this->getFunction($current);
|
||||
if (isset($current['file'])) {
|
||||
$current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
|
||||
}
|
||||
|
||||
unset($current['class'], $current['type'], $current['function'], $current['args']);
|
||||
|
||||
if (isset($current['twig'])) {
|
||||
$trace[] = $current['twig'];
|
||||
} else {
|
||||
$trace[] = ['call' => $class . $type . $function] + $current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$array = [
|
||||
'message' => $deprecated['message'],
|
||||
'file' => $deprecated['file'],
|
||||
'line' => $deprecated['line'],
|
||||
'trace' => $trace
|
||||
];
|
||||
|
||||
return [
|
||||
array_filter($array),
|
||||
$scope
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFunction($trace)
|
||||
{
|
||||
if (!isset($trace['function'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use Whoops\Handler\Handler;
|
||||
|
||||
class BareHandler extends Handler
|
||||
{
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$inspector = $this->getInspector();
|
||||
$code = $inspector->getException()->getCode();
|
||||
if ( ($code >= 400) && ($code < 600) )
|
||||
{
|
||||
$this->getRun()->sendHttpCode($code);
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Whoops;
|
||||
|
||||
class Errors
|
||||
{
|
||||
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 Whoops\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 Whoops\Handler\PrettyPageHandler;
|
||||
$error_page->setPageTitle('Crikey! There was an error...');
|
||||
$error_page->addResourcePath(GRAV_ROOT . '/system/assets');
|
||||
$error_page->addCustomCss('whoops.css');
|
||||
$whoops->pushHandler($error_page);
|
||||
break;
|
||||
case -1:
|
||||
$whoops->pushHandler(new BareHandler);
|
||||
break;
|
||||
default:
|
||||
$whoops->pushHandler(new SimplePageHandler);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
|
||||
$whoops->pushHandler(new Whoops\Handler\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,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Util\Misc;
|
||||
use Whoops\Util\TemplateHelper;
|
||||
|
||||
class SimplePageHandler extends Handler
|
||||
{
|
||||
private $searchPaths = array();
|
||||
private $resourceCache = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Add the default, local resource search path:
|
||||
$this->searchPaths[] = __DIR__ . '/Resources';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
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). ')'
|
||||
);
|
||||
}
|
||||
|
||||
public function addResourcePath($path)
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"'{$path}' is not a valid directory"
|
||||
);
|
||||
}
|
||||
|
||||
array_unshift($this->searchPaths, $path);
|
||||
}
|
||||
|
||||
public function getResourcePaths()
|
||||
{
|
||||
return $this->searchPaths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Errors
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
class SystemFacade extends \Whoops\Util\SystemFacade
|
||||
{
|
||||
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.
|
||||
*/
|
||||
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,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
|
||||
trait CompiledFile
|
||||
{
|
||||
/**
|
||||
* Get/set parsed file contents.
|
||||
*
|
||||
* @param mixed $var
|
||||
* @return string
|
||||
*/
|
||||
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) {
|
||||
return $this->decode($this->raw());
|
||||
}
|
||||
|
||||
$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.
|
||||
*/
|
||||
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,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends JsonFile
|
||||
{
|
||||
use CompiledFile;
|
||||
|
||||
/**
|
||||
* Decode RAW string into contents.
|
||||
*
|
||||
* @param string $var
|
||||
* @param bool $assoc
|
||||
* @return array mixed
|
||||
*/
|
||||
protected function decode($var, $assoc = true)
|
||||
{
|
||||
return (array)json_decode($var, $assoc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends MarkdownFile
|
||||
{
|
||||
use CompiledFile;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends YamlFile
|
||||
{
|
||||
use CompiledFile;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
|
||||
abstract class Archiver
|
||||
{
|
||||
protected $options = [
|
||||
'exclude_files' => ['.DS_Store'],
|
||||
'exclude_paths' => []
|
||||
];
|
||||
|
||||
protected $archive_file;
|
||||
|
||||
public static function create($compression)
|
||||
{
|
||||
if ($compression === 'zip') {
|
||||
return new ZipArchiver();
|
||||
}
|
||||
|
||||
return new ZipArchiver();
|
||||
}
|
||||
|
||||
public function setArchive($archive_file)
|
||||
{
|
||||
$this->archive_file = $archive_file;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setOptions($options)
|
||||
{
|
||||
// Set infinite PHP execution time if possible.
|
||||
if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) {
|
||||
set_time_limit(0);
|
||||
}
|
||||
|
||||
$this->options = $options + $this->options;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public abstract function compress($folder, callable $status = null);
|
||||
|
||||
public abstract function extract($destination, callable $status = null);
|
||||
|
||||
public abstract function addEmptyFolders($folders, callable $status = null);
|
||||
|
||||
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,497 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
abstract class Folder
|
||||
{
|
||||
/**
|
||||
* Recursively find the last modified time under given path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return int
|
||||
*/
|
||||
public static function lastModifiedFolder($path)
|
||||
{
|
||||
$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);
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
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')
|
||||
{
|
||||
$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)
|
||||
{
|
||||
$flags = \RecursiveDirectoryIterator::SKIP_DOTS;
|
||||
$files = [];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($path)) {
|
||||
$directory = $locator->getRecursiveIterator($path, $flags);
|
||||
} else {
|
||||
$directory = new \RecursiveDirectoryIterator($path, $flags);
|
||||
}
|
||||
|
||||
$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 mixed|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 === false) {
|
||||
throw new \RuntimeException("Path doesn't exist.");
|
||||
}
|
||||
|
||||
$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) {
|
||||
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 $ignore Ignore files matching pattern (regular expression).
|
||||
* @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
|
||||
* @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
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function mkdir($folder)
|
||||
{
|
||||
self::create($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @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);
|
||||
/** @var \DirectoryIterator $f */
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
class RecursiveDirectoryFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
protected static $root;
|
||||
protected static $ignore_folders;
|
||||
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;
|
||||
}
|
||||
|
||||
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,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
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,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Filesystem
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
class ZipArchiver extends Archiver
|
||||
{
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Form
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
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,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
abstract class AbstractCollection extends Iterator
|
||||
{
|
||||
public function toJson()
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
abstract class AbstractPackageCollection extends Iterator
|
||||
{
|
||||
protected $type;
|
||||
|
||||
public function toJson()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return json_encode($items);
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends Iterator
|
||||
{
|
||||
protected static $cache;
|
||||
|
||||
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,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
|
||||
class Package
|
||||
{
|
||||
/**
|
||||
* @var Data
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->data->get($key);
|
||||
}
|
||||
|
||||
public function __set($key, $value)
|
||||
{
|
||||
return $this->data->set($key, $value);
|
||||
}
|
||||
|
||||
public function __isset($key)
|
||||
{
|
||||
return isset($this->data->{$key});
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toJson();
|
||||
}
|
||||
|
||||
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
@@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Destination folder on which validation checks are applied
|
||||
* @var string
|
||||
*/
|
||||
protected static $target;
|
||||
|
||||
/**
|
||||
* @var int Error Code
|
||||
*/
|
||||
protected static $error = 0;
|
||||
|
||||
/**
|
||||
* @var int Zip Error Code
|
||||
*/
|
||||
protected static $error_zip = 0;
|
||||
|
||||
/**
|
||||
* @var string Post install message
|
||||
*/
|
||||
protected static $message = '';
|
||||
|
||||
/**
|
||||
* Default options for the install
|
||||
* @var array
|
||||
*/
|
||||
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 $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 bool|string
|
||||
*/
|
||||
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 = preg_replace('#\./$#', '', $zip->getNameIndex(0));
|
||||
$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 null|string
|
||||
*/
|
||||
private static function loadInstaller($installer_file_folder, $is_install)
|
||||
{
|
||||
$installer = null;
|
||||
|
||||
$installer_file_folder = rtrim($installer_file_folder, DS);
|
||||
|
||||
$install_file = $installer_file_folder . DS . 'install.php';
|
||||
|
||||
if (file_exists($install_file)) {
|
||||
require_once $install_file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (class_exists($class_name_alphanumeric)) {
|
||||
return $class_name_alphanumeric;
|
||||
}
|
||||
|
||||
return $installer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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') {
|
||||
foreach (glob($path . DS . '*') 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
|
||||
*/
|
||||
|
||||
public static function setError($error)
|
||||
{
|
||||
self::$error = $error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
/**
|
||||
* Class Licenses
|
||||
*
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Licenses
|
||||
{
|
||||
|
||||
/**
|
||||
* Regex to validate the format of a License
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $regex = '^(?:[A-F0-9]{8}-){3}(?:[A-F0-9]{8}){1}$';
|
||||
|
||||
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 $slug
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public static function get($slug = null)
|
||||
{
|
||||
$licenses = self::getLicenseFile();
|
||||
$data = (array)$licenses->content();
|
||||
$licenses->free();
|
||||
$slug = strtolower($slug);
|
||||
|
||||
if (!$slug) {
|
||||
return $data['licenses'] ?? [];
|
||||
}
|
||||
|
||||
return $data['licenses'][$slug] ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates the License format
|
||||
*
|
||||
* @param string $license
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate($license = null)
|
||||
{
|
||||
if (!is_string($license)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('#' . self::$regex. '#', $license);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the License File object
|
||||
*
|
||||
* @return \RocketTheme\Toolbox\File\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,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
abstract class AbstractPackageCollection extends BaseCollection
|
||||
{
|
||||
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,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
class Package extends BasePackage
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
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 mixed
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->settings['enabled'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends CachedCollection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$items = [
|
||||
'plugins' => new Plugins(),
|
||||
'themes' => new Themes()
|
||||
];
|
||||
|
||||
parent::__construct($items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 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,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'themes';
|
||||
|
||||
/**
|
||||
* Local Themes Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Grav::instance()['themes']->all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
class AbstractPackageCollection extends BaseCollection
|
||||
{
|
||||
/**
|
||||
* The cached data previously fetched
|
||||
* @var string
|
||||
*/
|
||||
protected $raw;
|
||||
|
||||
/**
|
||||
* The lifetime to store the entry in seconds
|
||||
* @var int
|
||||
*/
|
||||
private $lifetime = 86400;
|
||||
|
||||
protected $repository;
|
||||
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* AbstractPackageCollection constructor.
|
||||
*
|
||||
* @param null $repository
|
||||
* @param bool $refresh
|
||||
* @param 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 multisites
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,141 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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;
|
||||
|
||||
class GravCore extends AbstractPackageCollection
|
||||
{
|
||||
protected $repository = 'https://getgrav.org/downloads/grav.json';
|
||||
private $data;
|
||||
|
||||
private $version;
|
||||
private $date;
|
||||
private $min_php;
|
||||
|
||||
/**
|
||||
* @param bool $refresh
|
||||
* @param 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 $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 null|string
|
||||
*/
|
||||
public function getMinPHPVersion()
|
||||
{
|
||||
// If non min set, assume current PHP version
|
||||
if (null === $this->min_php) {
|
||||
$this->min_php = phpversion();
|
||||
}
|
||||
return $this->min_php;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this installation symlinked?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSymlink()
|
||||
{
|
||||
return is_link(GRAV_ROOT . DS . 'index.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends BasePackage implements \JsonSerializable
|
||||
{
|
||||
public function __construct($package, $package_type = null)
|
||||
{
|
||||
$data = new Data($package);
|
||||
parent::__construct($data, $package_type);
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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 extends CachedCollection
|
||||
{
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$items = [
|
||||
'plugins' => new Plugins($refresh, $callback),
|
||||
'themes' => new Themes($refresh, $callback)
|
||||
];
|
||||
|
||||
parent::__construct($items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'plugins';
|
||||
|
||||
protected $repository = 'https://getgrav.org/downloads/plugins.json';
|
||||
|
||||
/**
|
||||
* Local Plugins Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
parent::__construct($this->repository, $refresh, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
class Themes extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'themes';
|
||||
|
||||
protected $repository = 'https://getgrav.org/downloads/themes.json';
|
||||
|
||||
/**
|
||||
* Local Themes Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
parent::__construct($this->repository, $refresh, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Grav;
|
||||
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* The callback for the progress
|
||||
*
|
||||
* @var callable Either a function or callback in array notation
|
||||
*/
|
||||
public static $callback = null;
|
||||
|
||||
/**
|
||||
* Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $method = 'auto';
|
||||
|
||||
/**
|
||||
* Default parameters for `curl` and `fopen`
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $defaults = [
|
||||
|
||||
'curl' => [
|
||||
CURLOPT_REFERER => 'Grav GPM',
|
||||
CURLOPT_USERAGENT => 'Grav GPM',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HEADER => false,
|
||||
//CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
|
||||
/**
|
||||
* Example of callback parameters from within your own class
|
||||
*/
|
||||
//CURLOPT_NOPROGRESS => false,
|
||||
//CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
|
||||
],
|
||||
'fopen' => [
|
||||
'method' => 'GET',
|
||||
'user_agent' => 'Grav GPM',
|
||||
'max_redirects' => 5,
|
||||
'follow_location' => 1,
|
||||
'timeout' => 15,
|
||||
/* // this is set in the constructor since it's a setting
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
*/
|
||||
/**
|
||||
* Example of callback parameters from within your own class
|
||||
*/
|
||||
//'notification' => [$this, 'progress']
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the preferred method to use for making HTTP calls.
|
||||
*
|
||||
* @param string $method Default is `auto`
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public static function setMethod($method = 'auto')
|
||||
{
|
||||
if (!\in_array($method, ['auto', 'curl', 'fopen'], true)) {
|
||||
$method = 'auto';
|
||||
}
|
||||
|
||||
self::$method = $method;
|
||||
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the URL by using the preferred method
|
||||
*
|
||||
* @param string $uri URL to call
|
||||
* @param array $options An array of parameters for both `curl` and `fopen`
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
public static function get($uri = '', $options = [], $callback = null)
|
||||
{
|
||||
if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
|
||||
throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
|
||||
}
|
||||
|
||||
// check if this function is available, if so use it to stop any timeouts
|
||||
try {
|
||||
if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) {
|
||||
set_time_limit(0);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
$config = Grav::instance()['config'];
|
||||
$overrides = [];
|
||||
|
||||
// Override CA Bundle
|
||||
$caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
|
||||
if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
|
||||
$overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile;
|
||||
$overrides['fopen']['ssl']['capath'] = $caPathOrFile;
|
||||
} else {
|
||||
$overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile;
|
||||
$overrides['fopen']['ssl']['cafile'] = $caPathOrFile;
|
||||
}
|
||||
|
||||
// SSL Verify Peer and Proxy Setting
|
||||
$settings = [
|
||||
'method' => $config->get('system.gpm.method', self::$method),
|
||||
'verify_peer' => $config->get('system.gpm.verify_peer', true),
|
||||
// `system.proxy_url` is for fallback
|
||||
// introduced with 1.1.0-beta.1 probably safe to remove at some point
|
||||
'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
|
||||
];
|
||||
|
||||
if (!$settings['verify_peer']) {
|
||||
$overrides = array_replace_recursive([], $overrides, [
|
||||
'curl' => [
|
||||
CURLOPT_SSL_VERIFYPEER => $settings['verify_peer']
|
||||
],
|
||||
'fopen' => [
|
||||
'ssl' => [
|
||||
'verify_peer' => $settings['verify_peer'],
|
||||
'verify_peer_name' => $settings['verify_peer'],
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// Proxy Setting
|
||||
if ($settings['proxy_url']) {
|
||||
$proxy = parse_url($settings['proxy_url']);
|
||||
$fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
|
||||
|
||||
$overrides = array_replace_recursive([], $overrides, [
|
||||
'curl' => [
|
||||
CURLOPT_PROXY => $proxy['host'],
|
||||
CURLOPT_PROXYTYPE => 'HTTP'
|
||||
],
|
||||
'fopen' => [
|
||||
'proxy' => $fopen_proxy,
|
||||
'request_fulluri' => true
|
||||
]
|
||||
]);
|
||||
|
||||
if (isset($proxy['port'])) {
|
||||
$overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
|
||||
}
|
||||
|
||||
if (isset($proxy['user'], $proxy['pass'])) {
|
||||
$fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
|
||||
$overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
|
||||
$overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
|
||||
}
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(self::$defaults, $options, $overrides);
|
||||
$method = 'get' . ucfirst(strtolower($settings['method']));
|
||||
|
||||
self::$callback = $callback;
|
||||
return static::$method($uri, $options, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cURL is available
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isCurlAvailable()
|
||||
{
|
||||
return function_exists('curl_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the remote fopen request is enabled in PHP
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isFopenAvailable()
|
||||
{
|
||||
return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a remote file or not
|
||||
*
|
||||
* @param 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
|
||||
*/
|
||||
public static function progress()
|
||||
{
|
||||
static $filesize = null;
|
||||
|
||||
$args = func_get_args();
|
||||
$isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) === 'curl';
|
||||
|
||||
$notification_code = !$isCurlResource ? $args[0] : false;
|
||||
$bytes_transferred = $isCurlResource ? $args[2] : $args[4];
|
||||
|
||||
if ($isCurlResource) {
|
||||
$filesize = $args[1];
|
||||
} elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
|
||||
$filesize = $args[5];
|
||||
}
|
||||
|
||||
if ($bytes_transferred > 0) {
|
||||
if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) {
|
||||
|
||||
$progress = [
|
||||
'code' => $notification_code,
|
||||
'filesize' => $filesize,
|
||||
'transferred' => $bytes_transferred,
|
||||
'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1)
|
||||
];
|
||||
|
||||
if (self::$callback !== null) {
|
||||
call_user_func(self::$callback, $progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically picks the preferred method
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getAuto()
|
||||
{
|
||||
if (!ini_get('open_basedir') && self::isFopenAvailable()) {
|
||||
return self::getFopen(func_get_args());
|
||||
}
|
||||
|
||||
if (self::isCurlAvailable()) {
|
||||
return self::getCurl(func_get_args());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a HTTP request via fopen
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getFopen()
|
||||
{
|
||||
if (\count($args = func_get_args()) === 1) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
$uri = $args[0];
|
||||
$options = $args[1] ?? [];
|
||||
$callback = $args[2] ?? null;
|
||||
|
||||
if ($callback) {
|
||||
$options['fopen']['notification'] = ['self', 'progress'];
|
||||
}
|
||||
|
||||
if (isset($options['fopen']['ssl'])) {
|
||||
$ssl = $options['fopen']['ssl'];
|
||||
unset($options['fopen']['ssl']);
|
||||
|
||||
$stream = stream_context_create([
|
||||
'http' => $options['fopen'],
|
||||
'ssl' => $ssl
|
||||
], $options['fopen']);
|
||||
} else {
|
||||
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
|
||||
}
|
||||
|
||||
|
||||
$content = @file_get_contents($uri, false, $stream);
|
||||
|
||||
if ($content === false) {
|
||||
$code = null;
|
||||
// Function file_get_contents() creates local variable $http_response_header, check it
|
||||
if (isset($http_response_header)) {
|
||||
$code = explode(' ', $http_response_header[0] ?? '')[1] ?? null;
|
||||
}
|
||||
|
||||
switch ($code) {
|
||||
case '404':
|
||||
throw new \RuntimeException('Page not found');
|
||||
case '401':
|
||||
throw new \RuntimeException('Invalid LICENSE');
|
||||
default:
|
||||
throw new \RuntimeException("Error while trying to download (code: {$code}): {$uri}\n");
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a HTTP request via cURL
|
||||
*
|
||||
* @return string The response of the request
|
||||
*/
|
||||
private static function getCurl()
|
||||
{
|
||||
$args = func_get_args();
|
||||
$args = count($args) > 1 ? $args : array_shift($args);
|
||||
|
||||
$uri = $args[0];
|
||||
$options = $args[1] ?? [];
|
||||
$callback = $args[2] ?? null;
|
||||
|
||||
$ch = curl_init($uri);
|
||||
|
||||
$response = static::curlExecFollow($ch, $options, $callback);
|
||||
$errno = curl_errno($ch);
|
||||
|
||||
if ($errno) {
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error_message = curl_strerror($errno) . "\n" . curl_error($ch);
|
||||
|
||||
switch ($code) {
|
||||
case '404':
|
||||
throw new \RuntimeException('Page not found');
|
||||
case '401':
|
||||
throw new \RuntimeException('Invalid LICENSE');
|
||||
default:
|
||||
throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $ch
|
||||
* @param array $options
|
||||
* @param bool $callback
|
||||
*
|
||||
* @return bool|mixed
|
||||
*/
|
||||
private static function curlExecFollow($ch, $options, $callback)
|
||||
{
|
||||
if ($callback) {
|
||||
curl_setopt_array(
|
||||
$ch,
|
||||
[
|
||||
CURLOPT_NOPROGRESS => false,
|
||||
CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// no open_basedir set, we can proceed normally
|
||||
if (!ini_get('open_basedir')) {
|
||||
curl_setopt_array($ch, $options['curl']);
|
||||
return curl_exec($ch);
|
||||
}
|
||||
|
||||
$max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5;
|
||||
$options['curl'][CURLOPT_FOLLOWLOCATION] = false;
|
||||
|
||||
// open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
|
||||
curl_setopt_array($ch, $options['curl']);
|
||||
if ($max_redirects <= 0) {
|
||||
return curl_exec($ch);
|
||||
}
|
||||
|
||||
$uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
||||
$rch = curl_copy_handle($ch);
|
||||
|
||||
curl_setopt($rch, CURLOPT_HEADER, true);
|
||||
curl_setopt($rch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
|
||||
curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
do {
|
||||
curl_setopt($rch, CURLOPT_URL, $uri);
|
||||
$header = curl_exec($rch);
|
||||
|
||||
if (curl_errno($rch)) {
|
||||
$code = 0;
|
||||
} else {
|
||||
$code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
|
||||
if ($code === 301 || $code === 302 || $code === 303) {
|
||||
preg_match('/Location:(.*?)\n/', $header, $matches);
|
||||
$uri = trim(array_pop($matches));
|
||||
} else {
|
||||
$code = 0;
|
||||
}
|
||||
}
|
||||
} while ($code && --$max_redirects);
|
||||
|
||||
curl_close($rch);
|
||||
|
||||
if (!$max_redirects) {
|
||||
if ($max_redirects === null) {
|
||||
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $uri);
|
||||
|
||||
return curl_exec($ch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\GPM
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\GPM\Remote\GravCore;
|
||||
|
||||
/**
|
||||
* Class Upgrader
|
||||
*
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Upgrader
|
||||
{
|
||||
/**
|
||||
* Remote details about latest Grav version
|
||||
*
|
||||
* @var GravCore
|
||||
*/
|
||||
private $remote;
|
||||
|
||||
private $min_php;
|
||||
|
||||
/**
|
||||
* Creates a new GPM instance with Local and Remote packages available
|
||||
*
|
||||
* @param boolean $refresh Applies to Remote Packages only and forces a refetch of data
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
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 $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()
|
||||
{
|
||||
$current_php_version = phpversion();
|
||||
if (version_compare($current_php_version, $this->minPHPVersion(), '<')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum PHP version from remote
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
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 boolean 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 boolean True if Grav is symlinked, False otherwise.
|
||||
*/
|
||||
|
||||
public function isSymlink()
|
||||
{
|
||||
return $this->remote->isSymlink();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
abstract class Getters implements \ArrayAccess, \Countable
|
||||
{
|
||||
/**
|
||||
* Define variable used in getters.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $gettersVariable = null;
|
||||
|
||||
/**
|
||||
* Magic setter method
|
||||
*
|
||||
* @param mixed $offset Medium name value
|
||||
* @param mixed $value Medium value
|
||||
*/
|
||||
public function __set($offset, $value)
|
||||
{
|
||||
$this->offsetSet($offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter method
|
||||
*
|
||||
* @param mixed $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 mixed $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 mixed $offset The name value to unset
|
||||
*/
|
||||
public function __unset($offset)
|
||||
{
|
||||
$this->offsetUnset($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
if ($this->gettersVariable) {
|
||||
$var = $this->gettersVariable;
|
||||
|
||||
return isset($this->{$var}[$offset]);
|
||||
}
|
||||
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
if ($this->gettersVariable) {
|
||||
$var = $this->gettersVariable;
|
||||
|
||||
return $this->{$var}[$offset] ?? null;
|
||||
}
|
||||
|
||||
return $this->{$offset} ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if ($this->gettersVariable) {
|
||||
$var = $this->gettersVariable;
|
||||
$this->{$var}[$offset] = $value;
|
||||
} else {
|
||||
$this->{$offset} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Medium\ImageMedium;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Processors\AssetsProcessor;
|
||||
use Grav\Common\Processors\BackupsProcessor;
|
||||
use Grav\Common\Processors\ConfigurationProcessor;
|
||||
use Grav\Common\Processors\DebuggerAssetsProcessor;
|
||||
use Grav\Common\Processors\DebuggerProcessor;
|
||||
use Grav\Common\Processors\ErrorsProcessor;
|
||||
use Grav\Common\Processors\InitializeProcessor;
|
||||
use Grav\Common\Processors\LoggerProcessor;
|
||||
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\Framework\DI\Container;
|
||||
use Grav\Framework\Psr7\Response;
|
||||
use Grav\Framework\RequestHandler\RequestHandler;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\Event\EventDispatcher;
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'Grav\Common\Service\AccountsServiceProvider',
|
||||
'Grav\Common\Service\AssetsServiceProvider',
|
||||
'Grav\Common\Service\BackupsServiceProvider',
|
||||
'Grav\Common\Service\ConfigServiceProvider',
|
||||
'Grav\Common\Service\ErrorServiceProvider',
|
||||
'Grav\Common\Service\FilesystemServiceProvider',
|
||||
'Grav\Common\Service\InflectorServiceProvider',
|
||||
'Grav\Common\Service\LoggerServiceProvider',
|
||||
'Grav\Common\Service\OutputServiceProvider',
|
||||
'Grav\Common\Service\PagesServiceProvider',
|
||||
'Grav\Common\Service\RequestServiceProvider',
|
||||
'Grav\Common\Service\SessionServiceProvider',
|
||||
'Grav\Common\Service\StreamsServiceProvider',
|
||||
'Grav\Common\Service\TaskServiceProvider',
|
||||
'browser' => 'Grav\Common\Browser',
|
||||
'cache' => 'Grav\Common\Cache',
|
||||
'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
|
||||
'exif' => 'Grav\Common\Helpers\Exif',
|
||||
'plugins' => 'Grav\Common\Plugins',
|
||||
'scheduler' => 'Grav\Common\Scheduler\Scheduler',
|
||||
'taxonomy' => 'Grav\Common\Taxonomy',
|
||||
'themes' => 'Grav\Common\Themes',
|
||||
'twig' => 'Grav\Common\Twig\Twig',
|
||||
'uri' => 'Grav\Common\Uri',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array All middleware processors that are processed in $this->process()
|
||||
*/
|
||||
protected $middleware = [
|
||||
'configurationProcessor',
|
||||
'loggerProcessor',
|
||||
'errorsProcessor',
|
||||
'debuggerProcessor',
|
||||
'initializeProcessor',
|
||||
'pluginsProcessor',
|
||||
'themesProcessor',
|
||||
'requestProcessor',
|
||||
'tasksProcessor',
|
||||
'backupsProcessor',
|
||||
'schedulerProcessor',
|
||||
'assetsProcessor',
|
||||
'twigProcessor',
|
||||
'pagesProcessor',
|
||||
'debuggerAssetsProcessor',
|
||||
'renderProcessor',
|
||||
];
|
||||
|
||||
protected $initialized = [];
|
||||
|
||||
/**
|
||||
* Reset the Grav instance.
|
||||
*/
|
||||
public static function resetInstance()
|
||||
{
|
||||
if (self::$instance) {
|
||||
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 (!self::$instance) {
|
||||
self::$instance = static::load($values);
|
||||
} elseif ($values) {
|
||||
$instance = self::$instance;
|
||||
foreach ($values as $key => $value) {
|
||||
$instance->offsetSet($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Grav instance using specific environment.
|
||||
*
|
||||
* Initializes Grav streams by
|
||||
*
|
||||
* @param string|null $environment
|
||||
* @return $this
|
||||
*/
|
||||
public function setup(string $environment = null)
|
||||
{
|
||||
if (isset($this->initialized['setup'])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->initialized['setup'] = true;
|
||||
|
||||
$this->measureTime('_setup', 'Site Setup', function () use ($environment) {
|
||||
// Force environment if passed to the method.
|
||||
if ($environment) {
|
||||
Setup::$environment = $environment;
|
||||
}
|
||||
|
||||
$this['setup'];
|
||||
$this['streams'];
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
if (isset($this->initialized['process'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Grav if needed.
|
||||
$this->setup();
|
||||
|
||||
$this->initialized['process'] = true;
|
||||
|
||||
$container = new Container(
|
||||
[
|
||||
'configurationProcessor' => function () {
|
||||
return new ConfigurationProcessor($this);
|
||||
},
|
||||
'loggerProcessor' => function () {
|
||||
return new LoggerProcessor($this);
|
||||
},
|
||||
'errorsProcessor' => function () {
|
||||
return new ErrorsProcessor($this);
|
||||
},
|
||||
'debuggerProcessor' => function () {
|
||||
return new DebuggerProcessor($this);
|
||||
},
|
||||
'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 = function (ServerRequestInterface $request) {
|
||||
return new Response(404);
|
||||
};
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this['debugger'];
|
||||
|
||||
$collection = new RequestHandler($this->middleware, $default, $container);
|
||||
|
||||
$response = $collection->handle($this['request']);
|
||||
|
||||
$this->header($response);
|
||||
echo $response->getBody();
|
||||
|
||||
$debugger->render();
|
||||
|
||||
register_shutdown_function([$this, 'shutdown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system locale based on the language and configuration
|
||||
*/
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect browser to another location.
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int $code Redirection code (30x)
|
||||
*/
|
||||
public function redirect($route, $code = null)
|
||||
{
|
||||
/** @var Uri $uri */
|
||||
$uri = $this['uri'];
|
||||
|
||||
//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 (isset($this['session'])) {
|
||||
$this['session']->close();
|
||||
}
|
||||
|
||||
if ($uri->isExternal($route)) {
|
||||
$url = $route;
|
||||
} else {
|
||||
$url = rtrim($uri->rootUrl(), '/') . '/';
|
||||
|
||||
if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
|
||||
$url .= trim($route, '/'); // Remove trailing slash
|
||||
} else {
|
||||
$url .= ltrim($route, '/'); // Support trailing slash default routes
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: {$url}", true, $code);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect browser to another location taking language into account (preferred)
|
||||
*
|
||||
* @param string $route Internal route.
|
||||
* @param int $code Redirection code (30x)
|
||||
*/
|
||||
public function redirectLangSafe($route, $code = null)
|
||||
{
|
||||
if (!$this['uri']->isExternal($route)) {
|
||||
$this->redirect($this['pages']->route($route), $code);
|
||||
} else {
|
||||
$this->redirect($route, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response header.
|
||||
*
|
||||
* @param ResponseInterface|null $response
|
||||
*/
|
||||
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) {
|
||||
foreach ($values as $i => $value) {
|
||||
header($key . ': ' . $value, $i === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires an event with optional parameters.
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param Event $event
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function fireEvent($eventName, Event $event = null)
|
||||
{
|
||||
/** @var EventDispatcher $events */
|
||||
$events = $this['events'];
|
||||
|
||||
return $events->dispatch($eventName, $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the final content length for the page and flush the buffer
|
||||
*
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
if ($this['config']->get('system.debugger.shutdown.close_connection', true)) {
|
||||
// Flush the response and close the connection to allow time consuming tasks to be performed without leaving
|
||||
// the connection to the client open. This will make page loads to feel much faster.
|
||||
|
||||
// FastCGI allows us to flush all response data to the client and finish the request.
|
||||
$success = \function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
|
||||
|
||||
if (!$success) {
|
||||
// Unfortunately without FastCGI there is no way to force close the connection.
|
||||
// We need to ask browser to close the connection for us.
|
||||
if ($this['config']->get('system.cache.gzip')) {
|
||||
// Flush gzhandler buffer if gzip setting was enabled.
|
||||
ob_end_flush();
|
||||
|
||||
} else {
|
||||
// Without gzip we have no other choice than to prevent server from compressing the output.
|
||||
// This action turns off mod_deflate which would prevent us from closing the connection.
|
||||
if ($this['config']->get('system.cache.allow_webserver_gzip')) {
|
||||
header('Content-Encoding: identity');
|
||||
} else {
|
||||
header('Content-Encoding: none');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
$closure = $this->{$method};
|
||||
\call_user_func_array($closure, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->measureTime('_services', 'Services', function () use ($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
|
||||
*/
|
||||
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 PageInterface $page */
|
||||
$page = $this['pages']->dispatch($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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 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
|
||||
{
|
||||
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 (!self::$grav) {
|
||||
self::$grav = Grav::instance();
|
||||
}
|
||||
|
||||
return self::$grav;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
class Base32
|
||||
{
|
||||
protected static $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
protected static $base32Lookup = [
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$doc->loadHTML($html);
|
||||
$images = $doc->getElementsByTagName($tag);
|
||||
$excerpt = null;
|
||||
|
||||
foreach ($images as $image) {
|
||||
$attributes = [];
|
||||
foreach ($image->attributes as $name => $value) {
|
||||
$attributes[$name] = $value->value;
|
||||
}
|
||||
$excerpt = [
|
||||
'element' => [
|
||||
'name' => $image->tagName,
|
||||
'attributes' => $attributes
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
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 .= $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
|
||||
*/
|
||||
public static function processMediaActions($medium, $url, PageInterface $page = null)
|
||||
{
|
||||
$excerpts = new ExcerptsObject($page);
|
||||
|
||||
return $excerpts->processMediaActions($medium, $url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
class Exif
|
||||
{
|
||||
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('\PHPExif\Reader\Reader')) {
|
||||
$this->reader = \PHPExif\Reader\Reader::factory(\PHPExif\Reader\Reader::TYPE_NATIVE);
|
||||
} else {
|
||||
throw new \RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getReader()
|
||||
{
|
||||
if ($this->reader) {
|
||||
return $this->reader;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
class LogViewer
|
||||
{
|
||||
protected $pattern = '/\[(?P<date>.*)\] (?P<logger>\w+).(?P<level>\w+): (?P<message>.*[^ ]+) (?P<context>[^ ]+) (?P<extra>[^ ]+)/';
|
||||
|
||||
/**
|
||||
* Get the objects of a tailed file
|
||||
*
|
||||
* @param string $filepath
|
||||
* @param int $lines
|
||||
* @param bool $desc
|
||||
* @return array
|
||||
*/
|
||||
public function objectTail($filepath, $lines = 1, $desc = true)
|
||||
{
|
||||
$data = $this->tail($filepath, $lines);
|
||||
$tailed_log = explode(PHP_EOL, $data);
|
||||
$line_objects = [];
|
||||
|
||||
foreach ($tailed_log as $line) {
|
||||
$line_objects[] = $this->parse($line);
|
||||
}
|
||||
|
||||
return $desc ? $line_objects : array_reverse($line_objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized way to get just the last few entries of a log file
|
||||
*
|
||||
* @param string $filepath
|
||||
* @param int $lines
|
||||
* @return bool|string
|
||||
*/
|
||||
public function tail($filepath, $lines = 1) {
|
||||
|
||||
$f = @fopen($filepath, "rb");
|
||||
if ($f === false) return false;
|
||||
|
||||
else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
|
||||
|
||||
fseek($f, -1, SEEK_END);
|
||||
if (fread($f, 1) != "\n") $lines -= 1;
|
||||
|
||||
// Start reading
|
||||
$output = '';
|
||||
$chunk = '';
|
||||
// While we would like more
|
||||
while (ftell($f) > 0 && $lines >= 0) {
|
||||
// Figure out how far back we should jump
|
||||
$seek = min(ftell($f), $buffer);
|
||||
// Do the jump (backwards, relative to where we are)
|
||||
fseek($f, -$seek, SEEK_CUR);
|
||||
// Read a chunk and prepend it to our output
|
||||
$output = ($chunk = fread($f, $seek)) . $output;
|
||||
// Jump back to where we started reading
|
||||
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
|
||||
// Decrease our line counter
|
||||
$lines -= substr_count($chunk, "\n");
|
||||
}
|
||||
// While we have too many lines
|
||||
// (Because of buffer size we might have read too many)
|
||||
while ($lines++ < 0) {
|
||||
// Find first newline and remove all text before that
|
||||
$output = substr($output, strpos($output, "\n") + 1);
|
||||
}
|
||||
// Close file and return
|
||||
fclose($f);
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to get level color
|
||||
*
|
||||
* @param string $level
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function levelColor($level)
|
||||
{
|
||||
$colors = [
|
||||
'DEBUG' => 'green',
|
||||
'INFO' => 'cyan',
|
||||
'NOTICE' => 'yellow',
|
||||
'WARNING' => 'yellow',
|
||||
'ERROR' => 'red',
|
||||
'CRITICAL' => 'red',
|
||||
'ALERT' => 'red',
|
||||
'EMERGENCY' => 'magenta'
|
||||
];
|
||||
return $colors[$level] ?? 'white';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a monolog row into array bits
|
||||
*
|
||||
* @param string $line
|
||||
* @return array
|
||||
*/
|
||||
public function parse($line)
|
||||
{
|
||||
if( !is_string($line) || strlen($line) === 0) {
|
||||
return array();
|
||||
}
|
||||
preg_match($this->pattern, $line, $data);
|
||||
if (!isset($data['date'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
|
||||
if (is_array($matches) && isset($matches[1])) {
|
||||
$data['message'] = trim($matches[1]);
|
||||
$data['trace'] = trim($matches[2]);
|
||||
}
|
||||
|
||||
return array(
|
||||
'date' => \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
|
||||
'logger' => $data['logger'],
|
||||
'level' => $data['level'],
|
||||
'message' => $data['message'],
|
||||
'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
|
||||
'context' => json_decode($data['context'], true),
|
||||
'extra' => json_decode($data['extra'], true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse text of trace into an array of lines
|
||||
*
|
||||
* @param string $trace
|
||||
* @param int $rows
|
||||
* @return array
|
||||
*/
|
||||
public static function parseTrace($trace, $rows = 10)
|
||||
{
|
||||
$lines = array_filter(preg_split('/#\d*/m', $trace));
|
||||
return array_slice($lines, 0, $rows);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use DOMText;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMWordsIterator;
|
||||
use DOMLettersIterator;
|
||||
|
||||
/**
|
||||
* This file is part of https://github.com/Bluetel-Solutions/twig-truncate-extension
|
||||
*
|
||||
* Copyright (c) 2015 Bluetel Solutions developers@bluetel.co.uk
|
||||
* Copyright (c) 2015 Alex Wilson ajw@bluetel.co.uk
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class Truncator {
|
||||
|
||||
/**
|
||||
* Safely truncates HTML by a given number of words.
|
||||
* @param string $html Input HTML.
|
||||
* @param int $limit Limit to how many words we preserve.
|
||||
* @param string $ellipsis String to use as ellipsis (if any).
|
||||
* @return string Safe truncated HTML.
|
||||
*/
|
||||
public static function truncateWords($html, $limit = 0, $ellipsis = '')
|
||||
{
|
||||
if ($limit <= 0) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$doc = self::htmlToDomDocument($html);
|
||||
$container = $doc->getElementsByTagName('div')->item(0);
|
||||
$container = $container->parentNode->removeChild($container);
|
||||
|
||||
// Iterate over words.
|
||||
$words = new DOMWordsIterator($container);
|
||||
$truncated = false;
|
||||
foreach ($words as $word) {
|
||||
|
||||
// If we have exceeded the limit, we delete the remainder of the content.
|
||||
if ($words->key() >= $limit) {
|
||||
|
||||
// Grab current position.
|
||||
$currentWordPosition = $words->currentWordPosition();
|
||||
$curNode = $currentWordPosition[0];
|
||||
$offset = $currentWordPosition[1];
|
||||
$words = $currentWordPosition[2];
|
||||
|
||||
$curNode->nodeValue = substr(
|
||||
$curNode->nodeValue,
|
||||
0,
|
||||
$words[$offset][1] + strlen($words[$offset][0])
|
||||
);
|
||||
|
||||
self::removeProceedingNodes($curNode, $container);
|
||||
|
||||
if (!empty($ellipsis)) {
|
||||
self::insertEllipsis($curNode, $ellipsis);
|
||||
}
|
||||
|
||||
$truncated = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return original HTML if not truncated.
|
||||
if ($truncated) {
|
||||
$html = self::getCleanedHtml($doc, $container);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely truncates HTML by a given number of letters.
|
||||
* @param string $html Input HTML.
|
||||
* @param int $limit Limit to how many letters we preserve.
|
||||
* @param string $ellipsis String to use as ellipsis (if any).
|
||||
* @return string Safe truncated HTML.
|
||||
*/
|
||||
public static function truncateLetters($html, $limit = 0, $ellipsis = '')
|
||||
{
|
||||
if ($limit <= 0) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$doc = self::htmlToDomDocument($html);
|
||||
$container = $doc->getElementsByTagName('div')->item(0);
|
||||
$container = $container->parentNode->removeChild($container);
|
||||
|
||||
// Iterate over letters.
|
||||
$letters = new DOMLettersIterator($container);
|
||||
$truncated = false;
|
||||
foreach ($letters as $letter) {
|
||||
|
||||
// If we have exceeded the limit, we want to delete the remainder of this document.
|
||||
if ($letters->key() >= $limit) {
|
||||
|
||||
$currentText = $letters->currentTextPosition();
|
||||
$currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
|
||||
self::removeProceedingNodes($currentText[0], $container);
|
||||
|
||||
if (!empty($ellipsis)) {
|
||||
self::insertEllipsis($currentText[0], $ellipsis);
|
||||
}
|
||||
|
||||
$truncated = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return original HTML if not truncated.
|
||||
if ($truncated) {
|
||||
$html = self::getCleanedHtml($doc, $container);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a DOMDocument object from a string containing HTML.
|
||||
* @param string $html HTML to load
|
||||
* @returns DOMDocument Returns a DOMDocument object.
|
||||
*/
|
||||
public static function htmlToDomDocument($html)
|
||||
{
|
||||
if (!$html) {
|
||||
$html = '';
|
||||
}
|
||||
|
||||
// Transform multibyte entities which otherwise display incorrectly.
|
||||
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
|
||||
|
||||
// Internal errors enabled as HTML5 not fully supported.
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
// Instantiate new DOMDocument object, and then load in UTF-8 HTML.
|
||||
$dom = new DOMDocument();
|
||||
$dom->encoding = 'UTF-8';
|
||||
$dom->loadHTML("<div>$html</div>");
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all nodes after the current node.
|
||||
* @param DOMNode|DOMElement $domNode
|
||||
* @param DOMNode|DOMElement $topNode
|
||||
* @return void
|
||||
*/
|
||||
private static function removeProceedingNodes($domNode, $topNode)
|
||||
{
|
||||
$nextNode = $domNode->nextSibling;
|
||||
|
||||
if ($nextNode !== null) {
|
||||
self::removeProceedingNodes($nextNode, $topNode);
|
||||
$domNode->parentNode->removeChild($nextNode);
|
||||
} else {
|
||||
//scan upwards till we find a sibling
|
||||
$curNode = $domNode->parentNode;
|
||||
while ($curNode !== $topNode) {
|
||||
if ($curNode->nextSibling !== null) {
|
||||
$curNode = $curNode->nextSibling;
|
||||
self::removeProceedingNodes($curNode, $topNode);
|
||||
$curNode->parentNode->removeChild($curNode);
|
||||
break;
|
||||
}
|
||||
$curNode = $curNode->parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean extra code
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
* @param $container
|
||||
* @return string
|
||||
*/
|
||||
private static function getCleanedHTML(DOMDocument $doc, $container)
|
||||
{
|
||||
while ($doc->firstChild) {
|
||||
$doc->removeChild($doc->firstChild);
|
||||
}
|
||||
|
||||
while ($container->firstChild ) {
|
||||
$doc->appendChild($container->firstChild);
|
||||
}
|
||||
|
||||
$html = trim($doc->saveHTML());
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an ellipsis
|
||||
* @param DOMNode|DOMElement $domNode Element to insert after.
|
||||
* @param string $ellipsis Text used to suffix our document.
|
||||
* @return void
|
||||
*/
|
||||
private static function insertEllipsis($domNode, $ellipsis)
|
||||
{
|
||||
$avoid = array('a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5'); //html tags to avoid appending the ellipsis to
|
||||
|
||||
if ($domNode->parentNode->parentNode !== null && in_array($domNode->parentNode->nodeName, $avoid, true)) {
|
||||
// Append as text node to parent instead
|
||||
$textNode = new DOMText($ellipsis);
|
||||
|
||||
if ($domNode->parentNode->parentNode->nextSibling) {
|
||||
$domNode->parentNode->parentNode->insertBefore($textNode, $domNode->parentNode->parentNode->nextSibling);
|
||||
} else {
|
||||
$domNode->parentNode->parentNode->appendChild($textNode);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Append to current node
|
||||
$domNode->nodeValue = rtrim($domNode->nodeValue) . $ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function truncate(
|
||||
$text,
|
||||
$length = 100,
|
||||
$ending = '...',
|
||||
$exact = false,
|
||||
$considerHtml = true
|
||||
) {
|
||||
if ($considerHtml) {
|
||||
// if the plain text is shorter than the maximum length, return the whole text
|
||||
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
|
||||
return $text;
|
||||
}
|
||||
// splits all html-tags to scanable lines
|
||||
preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
|
||||
$total_length = strlen($ending);
|
||||
$open_tags = array();
|
||||
$truncate = '';
|
||||
foreach ($lines as $line_matchings) {
|
||||
// if there is any html-tag in this line, handle it and add it (uncounted) to the output
|
||||
if (!empty($line_matchings[1])) {
|
||||
// if it's an "empty element" with or without xhtml-conform closing slash
|
||||
if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
|
||||
// do nothing
|
||||
// if tag is a closing tag
|
||||
} else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
|
||||
// delete tag from $open_tags list
|
||||
$pos = array_search($tag_matchings[1], $open_tags);
|
||||
if ($pos !== false) {
|
||||
unset($open_tags[$pos]);
|
||||
}
|
||||
// if tag is an opening tag
|
||||
} else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
|
||||
// add tag to the beginning of $open_tags list
|
||||
array_unshift($open_tags, strtolower($tag_matchings[1]));
|
||||
}
|
||||
// add html-tag to $truncate'd text
|
||||
$truncate .= $line_matchings[1];
|
||||
}
|
||||
// calculate the length of the plain text part of the line; handle entities as one character
|
||||
$content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
|
||||
if ($total_length+$content_length> $length) {
|
||||
// the number of characters which are left
|
||||
$left = $length - $total_length;
|
||||
$entities_length = 0;
|
||||
// search for html entities
|
||||
if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) {
|
||||
// calculate the real length of all entities in the legal range
|
||||
foreach ($entities[0] as $entity) {
|
||||
if ($entity[1]+1-$entities_length <= $left) {
|
||||
$left--;
|
||||
$entities_length += strlen($entity[0]);
|
||||
} else {
|
||||
// no more characters left
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$truncate .= substr($line_matchings[2], 0, $left+$entities_length);
|
||||
// maximum lenght is reached, so get off the loop
|
||||
break;
|
||||
} else {
|
||||
$truncate .= $line_matchings[2];
|
||||
$total_length += $content_length;
|
||||
}
|
||||
// if the maximum length is reached, get off the loop
|
||||
if($total_length>= $length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (strlen($text) <= $length) {
|
||||
return $text;
|
||||
} else {
|
||||
$truncate = substr($text, 0, $length - strlen($ending));
|
||||
}
|
||||
}
|
||||
// if the words shouldn't be cut in the middle...
|
||||
if (!$exact) {
|
||||
// ...search the last occurance of a space...
|
||||
$spacepos = strrpos($truncate, ' ');
|
||||
if (isset($spacepos)) {
|
||||
// ...and cut the text in this position
|
||||
$truncate = substr($truncate, 0, $spacepos);
|
||||
}
|
||||
}
|
||||
// add the defined ending to the text
|
||||
$truncate .= $ending;
|
||||
if($considerHtml) {
|
||||
// close all unclosed html-tags
|
||||
foreach ($open_tags as $tag) {
|
||||
$truncate .= '</' . $tag . '>';
|
||||
}
|
||||
}
|
||||
return $truncate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Helpers
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\File\MarkdownFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class YamlLinter
|
||||
{
|
||||
public static function lint()
|
||||
{
|
||||
$errors = static::lintConfig();
|
||||
$errors = $errors + static::lintPages();
|
||||
$errors = $errors + static::lintBlueprints();
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public static function lintPages()
|
||||
{
|
||||
return static::recurseFolder('page://');
|
||||
}
|
||||
|
||||
public static function lintConfig()
|
||||
{
|
||||
return static::recurseFolder('config://');
|
||||
}
|
||||
|
||||
public static function lintBlueprints()
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$current_theme = Grav::instance()['config']->get('system.pages.theme');
|
||||
$theme_path = 'themes://' . $current_theme . '/blueprints';
|
||||
|
||||
$locator->addPath('blueprints', '', [$theme_path]);
|
||||
return static::recurseFolder('blueprints://');
|
||||
}
|
||||
|
||||
public static function recurseFolder($path, $extensions = 'md|yaml')
|
||||
{
|
||||
$lint_errors = [];
|
||||
|
||||
/** @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 {
|
||||
Yaml::parse(static::extractYaml($filepath));
|
||||
} catch (\Exception $e) {
|
||||
$lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $lint_errors;
|
||||
}
|
||||
|
||||
protected static function extractYaml($path)
|
||||
{
|
||||
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
||||
if ($extension === 'md') {
|
||||
$file = MarkdownFile::instance($path);
|
||||
$contents = $file->frontmatter();
|
||||
} else {
|
||||
$contents = file_get_contents($path);
|
||||
}
|
||||
return $contents;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* This file was originally part of the Akelos Framework
|
||||
*/
|
||||
|
||||
class Inflector
|
||||
{
|
||||
protected static $plural;
|
||||
protected static $singular;
|
||||
protected static $uncountable;
|
||||
protected static $irregular;
|
||||
protected static $ordinals;
|
||||
|
||||
public static function init()
|
||||
{
|
||||
if (empty(static::$plural)) {
|
||||
$language = Grav::instance()['language'];
|
||||
static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true) ?: [];
|
||||
static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true) ?: [];
|
||||
static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true) ?: [];
|
||||
static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true) ?: [];
|
||||
static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true) ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluralizes English nouns.
|
||||
*
|
||||
* @param string $word English noun to pluralize
|
||||
* @param int $count The count
|
||||
*
|
||||
* @return string Plural noun
|
||||
*/
|
||||
public static function pluralize($word, $count = 2)
|
||||
{
|
||||
static::init();
|
||||
|
||||
if ((int)$count === 1) {
|
||||
return $word;
|
||||
}
|
||||
|
||||
$lowercased_word = strtolower($word);
|
||||
|
||||
foreach (static::$uncountable as $_uncountable) {
|
||||
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (static::$irregular as $_plural => $_singular) {
|
||||
if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
|
||||
return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (static::$plural as $rule => $replacement) {
|
||||
if (preg_match($rule, $word)) {
|
||||
return preg_replace($rule, $replacement, $word);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Singularizes English nouns.
|
||||
*
|
||||
* @param string $word English noun to singularize
|
||||
* @param int $count
|
||||
*
|
||||
* @return string Singular noun.
|
||||
*/
|
||||
public static function singularize($word, $count = 1)
|
||||
{
|
||||
static::init();
|
||||
|
||||
if ((int)$count !== 1) {
|
||||
return $word;
|
||||
}
|
||||
|
||||
$lowercased_word = strtolower($word);
|
||||
foreach (static::$uncountable as $_uncountable) {
|
||||
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (static::$irregular as $_plural => $_singular) {
|
||||
if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
|
||||
return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (static::$singular as $rule => $replacement) {
|
||||
if (preg_match($rule, $word)) {
|
||||
return preg_replace($rule, $replacement, $word);
|
||||
}
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an underscored or CamelCase word into a English
|
||||
* sentence.
|
||||
*
|
||||
* The titleize public function converts text like "WelcomePage",
|
||||
* "welcome_page" or "welcome page" to this "Welcome
|
||||
* Page".
|
||||
* If second parameter is set to 'first' it will only
|
||||
* capitalize the first character of the title.
|
||||
*
|
||||
* @param string $word Word to format as tile
|
||||
* @param string $uppercase If set to 'first' it will only uppercase the
|
||||
* first character. Otherwise it will uppercase all
|
||||
* the words in the title.
|
||||
*
|
||||
* @return string Text formatted as title
|
||||
*/
|
||||
public static function titleize($word, $uppercase = '')
|
||||
{
|
||||
$uppercase = $uppercase === 'first' ? 'ucfirst' : 'ucwords';
|
||||
|
||||
return $uppercase(static::humanize(static::underscorize($word)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns given word as CamelCased
|
||||
*
|
||||
* Converts a word like "send_email" to "SendEmail". It
|
||||
* will remove non alphanumeric character from the word, so
|
||||
* "who's online" will be converted to "WhoSOnline"
|
||||
*
|
||||
* @see variablize
|
||||
*
|
||||
* @param string $word Word to convert to camel case
|
||||
*
|
||||
* @return string UpperCamelCasedWord
|
||||
*/
|
||||
public static function camelize($word)
|
||||
{
|
||||
return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a word "into_it_s_underscored_version"
|
||||
*
|
||||
* Convert any "CamelCased" or "ordinary Word" into an
|
||||
* "underscored_word".
|
||||
*
|
||||
* This can be really useful for creating friendly URLs.
|
||||
*
|
||||
* @param string $word Word to underscore
|
||||
*
|
||||
* @return string Underscored word
|
||||
*/
|
||||
public static function underscorize($word)
|
||||
{
|
||||
$regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word);
|
||||
$regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1);
|
||||
$regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '_', $regex2);
|
||||
|
||||
return strtolower($regex3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a word "into-it-s-hyphenated-version"
|
||||
*
|
||||
* Convert any "CamelCased" or "ordinary Word" into an
|
||||
* "hyphenated-word".
|
||||
*
|
||||
* This can be really useful for creating friendly URLs.
|
||||
*
|
||||
* @param string $word Word to hyphenate
|
||||
*
|
||||
* @return string hyphenized word
|
||||
*/
|
||||
public static function hyphenize($word)
|
||||
{
|
||||
$regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word);
|
||||
$regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1);
|
||||
$regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2);
|
||||
$regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3);
|
||||
|
||||
return strtolower($regex4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable string from $word
|
||||
*
|
||||
* Returns a human-readable string from $word, by replacing
|
||||
* underscores with a space, and by upper-casing the initial
|
||||
* character by default.
|
||||
*
|
||||
* If you need to uppercase all the words you just have to
|
||||
* pass 'all' as a second parameter.
|
||||
*
|
||||
* @param string $word String to "humanize"
|
||||
* @param string $uppercase If set to 'all' it will uppercase all the words
|
||||
* instead of just the first one.
|
||||
*
|
||||
* @return string Human-readable word
|
||||
*/
|
||||
public static function humanize($word, $uppercase = '')
|
||||
{
|
||||
$uppercase = $uppercase === 'all' ? 'ucwords' : 'ucfirst';
|
||||
|
||||
return $uppercase(str_replace('_', ' ', preg_replace('/_id$/', '', $word)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as camelize but first char is underscored
|
||||
*
|
||||
* Converts a word like "send_email" to "sendEmail". It
|
||||
* will remove non alphanumeric character from the word, so
|
||||
* "who's online" will be converted to "whoSOnline"
|
||||
*
|
||||
* @see camelize
|
||||
*
|
||||
* @param string $word Word to lowerCamelCase
|
||||
*
|
||||
* @return string Returns a lowerCamelCasedWord
|
||||
*/
|
||||
public static function variablize($word)
|
||||
{
|
||||
$word = static::camelize($word);
|
||||
|
||||
return strtolower($word[0]) . substr($word, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a class name to its table name according to rails
|
||||
* naming conventions.
|
||||
*
|
||||
* Converts "Person" to "people"
|
||||
*
|
||||
* @see classify
|
||||
*
|
||||
* @param string $class_name Class name for getting related table_name.
|
||||
*
|
||||
* @return string plural_table_name
|
||||
*/
|
||||
public static function tableize($class_name)
|
||||
{
|
||||
return static::pluralize(static::underscorize($class_name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a table name to its class name according to rails
|
||||
* naming conventions.
|
||||
*
|
||||
* Converts "people" to "Person"
|
||||
*
|
||||
* @see tableize
|
||||
*
|
||||
* @param string $table_name Table name for getting related ClassName.
|
||||
*
|
||||
* @return string SingularClassName
|
||||
*/
|
||||
public static function classify($table_name)
|
||||
{
|
||||
return static::camelize(static::singularize($table_name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts number to its ordinal English form.
|
||||
*
|
||||
* This method converts 13 to 13th, 2 to 2nd ...
|
||||
*
|
||||
* @param int $number Number to get its ordinal value
|
||||
*
|
||||
* @return string Ordinal representation of given string.
|
||||
*/
|
||||
public static function ordinalize($number)
|
||||
{
|
||||
static::init();
|
||||
|
||||
if (\in_array($number % 100, range(11, 13), true)) {
|
||||
return $number . static::$ordinals['default'];
|
||||
}
|
||||
|
||||
switch ($number % 10) {
|
||||
case 1:
|
||||
return $number . static::$ordinals['first'];
|
||||
case 2:
|
||||
return $number . static::$ordinals['second'];
|
||||
case 3:
|
||||
return $number . static::$ordinals['third'];
|
||||
default:
|
||||
return $number . static::$ordinals['default'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number of days to a number of months
|
||||
*
|
||||
* @param int $days
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function monthize($days)
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$end = new \DateTime();
|
||||
|
||||
$duration = new \DateInterval("P{$days}D");
|
||||
|
||||
$diff = $end->add($duration)->diff($now);
|
||||
|
||||
// handle years
|
||||
if ($diff->y > 0) {
|
||||
$diff->m += 12 * $diff->y;
|
||||
}
|
||||
|
||||
return $diff->m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
use RocketTheme\Toolbox\ArrayTraits\ArrayAccessWithGetters;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Iterator as ArrayIterator;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Constructor;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Countable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Serializable;
|
||||
|
||||
class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
|
||||
{
|
||||
use Constructor, ArrayAccessWithGetters, ArrayIterator, Countable, Serializable, Export;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $items = [];
|
||||
|
||||
/**
|
||||
* Convert function calls for the existing keys into their values.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $args
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($key, $args)
|
||||
{
|
||||
return $this->items[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the iterator.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this as $key => $value) {
|
||||
if (\is_object($value)) {
|
||||
$this->{$key} = clone $this->{$key};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convents iterator to a comma separated list.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return implode(',', $this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from the list.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function remove($key)
|
||||
{
|
||||
$this->offsetUnset($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return previous item.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prev()
|
||||
{
|
||||
return prev($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return nth item.
|
||||
*
|
||||
* @param int $key
|
||||
*
|
||||
* @return mixed|bool
|
||||
*/
|
||||
public function nth($key)
|
||||
{
|
||||
$items = array_keys($this->items);
|
||||
|
||||
return isset($items[$key]) ? $this->offsetGet($items[$key]) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first item
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
$items = array_keys($this->items);
|
||||
|
||||
return $this->offsetGet(array_shift($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last item
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function last()
|
||||
{
|
||||
$items = array_keys($this->items);
|
||||
|
||||
return $this->offsetGet(array_pop($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the Iterator
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reverse()
|
||||
{
|
||||
$this->items = array_reverse($this->items);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $needle Searched value.
|
||||
*
|
||||
* @return string|bool Key if found, otherwise false.
|
||||
*/
|
||||
public function indexOf($needle)
|
||||
{
|
||||
foreach (array_values($this->items) as $key => $value) {
|
||||
if ($value === $needle) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle items.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function shuffle()
|
||||
{
|
||||
$keys = array_keys($this->items);
|
||||
shuffle($keys);
|
||||
|
||||
$new = [];
|
||||
foreach ($keys as $key) {
|
||||
$new[$key] = $this->items[$key];
|
||||
}
|
||||
|
||||
$this->items = $new;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice the list.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function slice($offset, $length = null)
|
||||
{
|
||||
$this->items = \array_slice($this->items, $offset, $length);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick one or more random entries.
|
||||
*
|
||||
* @param int $num Specifies how many entries should be picked.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function random($num = 1)
|
||||
{
|
||||
$count = \count($this->items);
|
||||
if ($num > $count) {
|
||||
$num = $count;
|
||||
}
|
||||
|
||||
$this->items = array_intersect_key($this->items, array_flip((array)array_rand($this->items, $num)));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append new elements to the list.
|
||||
*
|
||||
* @param array|Iterator $items Items to be appended. Existing keys will be overridden with the new values.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function append($items)
|
||||
{
|
||||
if ($items instanceof static) {
|
||||
$items = $items->toArray();
|
||||
}
|
||||
$this->items = array_merge($this->items, (array)$items);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter elements from the list
|
||||
*
|
||||
* @param callable|null $callback A function the receives ($value, $key) and must return a boolean to indicate
|
||||
* filter status
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function filter(callable $callback = null)
|
||||
{
|
||||
foreach ($this->items as $key => $value) {
|
||||
if (
|
||||
(!$callback && !(bool)$value) ||
|
||||
($callback && !$callback($value, $key))
|
||||
) {
|
||||
unset($this->items[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorts elements from the list and returns a copy of the list in the proper order
|
||||
*
|
||||
* @param callable|null $callback
|
||||
*
|
||||
* @param bool $desc
|
||||
*
|
||||
* @return $this|array
|
||||
* @internal param bool $asc
|
||||
*
|
||||
*/
|
||||
public function sort(callable $callback = null, $desc = false)
|
||||
{
|
||||
if (!$callback || !\is_callable($callback)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$items = $this->items;
|
||||
uasort($items, $callback);
|
||||
|
||||
return !$desc ? $items : array_reverse($items, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Language
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Language;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Config\Config;
|
||||
use Negotiation\AcceptLanguage;
|
||||
use Negotiation\LanguageNegotiator;
|
||||
|
||||
class Language
|
||||
{
|
||||
protected $grav;
|
||||
protected $enabled = true;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $languages = [];
|
||||
protected $page_extensions = [];
|
||||
protected $fallback_languages = [];
|
||||
protected $default;
|
||||
protected $active = null;
|
||||
|
||||
/** @var Config $config */
|
||||
protected $config;
|
||||
|
||||
protected $http_accept_language;
|
||||
protected $lang_in_url = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Grav\Common\Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
$this->grav = $grav;
|
||||
$this->config = $grav['config'];
|
||||
$this->languages = $this->config->get('system.languages.supported', []);
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the default and enabled languages
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$default = $this->config->get('system.languages.default_lang');
|
||||
if (isset($default) && $this->validate($default)) {
|
||||
$this->default = $default;
|
||||
} else {
|
||||
$this->default = reset($this->languages);
|
||||
}
|
||||
|
||||
$this->page_extensions = null;
|
||||
|
||||
if (empty($this->languages)) {
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that languages are enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function enabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the array of supported languages
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguages()
|
||||
{
|
||||
return $this->languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current supported languages manually
|
||||
*
|
||||
* @param array $langs
|
||||
*/
|
||||
public function setLanguages($langs)
|
||||
{
|
||||
$this->languages = $langs;
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pipe-separated string of available languages
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAvailable()
|
||||
{
|
||||
$languagesArray = $this->languages; //Make local copy
|
||||
|
||||
$languagesArray = array_map(function($value) {
|
||||
return preg_quote($value);
|
||||
}, $languagesArray);
|
||||
|
||||
sort($languagesArray);
|
||||
|
||||
return implode('|', array_reverse($languagesArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets language, active if set, else default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->active ?: $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current default language
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDefault()
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default language manually
|
||||
*
|
||||
* @param string $lang
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setDefault($lang)
|
||||
{
|
||||
if ($this->validate($lang)) {
|
||||
$this->default = $lang;
|
||||
|
||||
return $lang;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current active language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActive()
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets active language manually
|
||||
*
|
||||
* @param string $lang
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function setActive($lang)
|
||||
{
|
||||
if ($this->validate($lang)) {
|
||||
$this->active = $lang;
|
||||
|
||||
return $lang;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active language based on the first part of the URL
|
||||
*
|
||||
* @param string $uri
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function setActiveFromUri($uri)
|
||||
{
|
||||
$regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i';
|
||||
|
||||
// if languages set
|
||||
if ($this->enabled()) {
|
||||
// Try setting language from prefix of URL (/en/blah/blah).
|
||||
if (preg_match($regex, $uri, $matches)) {
|
||||
$this->lang_in_url = true;
|
||||
$this->active = $matches[2];
|
||||
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
|
||||
|
||||
// Store in session if language is different.
|
||||
if (isset($this->grav['session']) && $this->grav['session']->isStarted()
|
||||
&& $this->config->get('system.languages.session_store_active', true)
|
||||
&& $this->grav['session']->active_language != $this->active
|
||||
) {
|
||||
$this->grav['session']->active_language = $this->active;
|
||||
}
|
||||
} else {
|
||||
// Try getting language from the session, else no active.
|
||||
if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
|
||||
$this->config->get('system.languages.session_store_active', true)) {
|
||||
$this->active = $this->grav['session']->active_language ?: null;
|
||||
}
|
||||
// if still null, try from http_accept_language header
|
||||
if ($this->active === null &&
|
||||
$this->config->get('system.languages.http_accept_language') &&
|
||||
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) {
|
||||
|
||||
$negotiator = new LanguageNegotiator();
|
||||
$best_language = $negotiator->getBest($accept, $this->languages);
|
||||
|
||||
if ($best_language instanceof AcceptLanguage) {
|
||||
$this->active = $best_language->getType();
|
||||
} else {
|
||||
$this->active = $this->getDefault();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL prefix based on configuration
|
||||
*
|
||||
* @param string|null $lang
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguageURLPrefix($lang = null)
|
||||
{
|
||||
// if active lang is not passed in, use current active
|
||||
if (!$lang) {
|
||||
$lang = $this->getLanguage();
|
||||
}
|
||||
|
||||
return $this->isIncludeDefaultLanguage($lang) ? '/' . $lang : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if language is default and language should be included in the URL
|
||||
*
|
||||
* @param string|null $lang
|
||||
* @return bool
|
||||
*/
|
||||
public function isIncludeDefaultLanguage($lang = null)
|
||||
{
|
||||
// if active lang is not passed in, use current active
|
||||
if (!$lang) {
|
||||
$lang = $this->getLanguage();
|
||||
}
|
||||
|
||||
return !($this->default === $lang && $this->config->get('system.languages.include_default_lang') === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple getter to tell if a language was found in the URL
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLanguageInUrl()
|
||||
{
|
||||
return (bool) $this->lang_in_url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets an array of valid extensions with active first, then fallback extensions
|
||||
*
|
||||
* @param string|null $file_ext
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFallbackPageExtensions($file_ext = null)
|
||||
{
|
||||
if (empty($this->page_extensions)) {
|
||||
if (!$file_ext) {
|
||||
$file_ext = CONTENT_EXT;
|
||||
}
|
||||
|
||||
if ($this->enabled()) {
|
||||
$valid_lang_extensions = [];
|
||||
foreach ($this->languages as $lang) {
|
||||
$valid_lang_extensions[] = '.' . $lang . $file_ext;
|
||||
}
|
||||
|
||||
if ($this->active) {
|
||||
$active_extension = '.' . $this->active . $file_ext;
|
||||
$key = \array_search($active_extension, $valid_lang_extensions, true);
|
||||
|
||||
// Default behavior is to find any language other than active
|
||||
if ($this->config->get('system.languages.pages_fallback_only')) {
|
||||
$slice = \array_slice($valid_lang_extensions, 0, $key+1);
|
||||
$valid_lang_extensions = array_reverse($slice);
|
||||
} else {
|
||||
unset($valid_lang_extensions[$key]);
|
||||
array_unshift($valid_lang_extensions, $active_extension);
|
||||
}
|
||||
}
|
||||
$valid_lang_extensions[] = $file_ext;
|
||||
$this->page_extensions = $valid_lang_extensions;
|
||||
} else {
|
||||
$this->page_extensions = (array)$file_ext;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->page_extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the page_extensions value.
|
||||
*
|
||||
* Useful to re-initialize the pages and change site language at runtime, example:
|
||||
*
|
||||
* ```
|
||||
* $this->grav['language']->setActive('it');
|
||||
* $this->grav['language']->resetFallbackPageExtensions();
|
||||
* $this->grav['pages']->init();
|
||||
* ```
|
||||
*/
|
||||
public function resetFallbackPageExtensions()
|
||||
{
|
||||
$this->page_extensions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of languages with active first, then fallback languages
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFallbackLanguages()
|
||||
{
|
||||
if (empty($this->fallback_languages)) {
|
||||
if ($this->enabled()) {
|
||||
$fallback_languages = $this->languages;
|
||||
|
||||
if ($this->active) {
|
||||
$active_extension = $this->active;
|
||||
$key = \array_search($active_extension, $fallback_languages, true);
|
||||
unset($fallback_languages[$key]);
|
||||
array_unshift($fallback_languages, $active_extension);
|
||||
}
|
||||
$this->fallback_languages = $fallback_languages;
|
||||
}
|
||||
// always add english in case a translation doesn't exist
|
||||
$this->fallback_languages[] = 'en';
|
||||
}
|
||||
|
||||
return $this->fallback_languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the language is valid and supported
|
||||
*
|
||||
* @param string $lang
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate($lang)
|
||||
{
|
||||
return \in_array($lang, $this->languages, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a key and possibly arguments into a string using current lang and fallbacks
|
||||
*
|
||||
* @param string|array $args The first argument is the lookup key value
|
||||
* Other arguments can be passed and replaced in the translation with sprintf syntax
|
||||
* @param array $languages
|
||||
* @param bool $array_support
|
||||
* @param bool $html_out
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function translate($args, array $languages = null, $array_support = false, $html_out = false)
|
||||
{
|
||||
if (\is_array($args)) {
|
||||
$lookup = array_shift($args);
|
||||
} else {
|
||||
$lookup = $args;
|
||||
$args = [];
|
||||
}
|
||||
|
||||
if ($this->config->get('system.languages.translations', true)) {
|
||||
if ($this->enabled() && $lookup) {
|
||||
if (empty($languages)) {
|
||||
if ($this->config->get('system.languages.translations_fallback', true)) {
|
||||
$languages = $this->getFallbackLanguages();
|
||||
} else {
|
||||
$languages = (array)$this->getLanguage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$languages = ['en'];
|
||||
}
|
||||
|
||||
foreach ((array)$languages as $lang) {
|
||||
$translation = $this->getTranslation($lang, $lookup, $array_support);
|
||||
|
||||
if ($translation) {
|
||||
if (\count($args) >= 1) {
|
||||
return vsprintf($translation, $args);
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($html_out) {
|
||||
return '<span class="untranslated">' . $lookup . '</span>';
|
||||
}
|
||||
|
||||
return $lookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Array
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $index
|
||||
* @param array|null $languages
|
||||
* @param bool $html_out
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function translateArray($key, $index, $languages = null, $html_out = false)
|
||||
{
|
||||
if ($this->config->get('system.languages.translations', true)) {
|
||||
if ($this->enabled() && $key) {
|
||||
if (empty($languages)) {
|
||||
if ($this->config->get('system.languages.translations_fallback', true)) {
|
||||
$languages = $this->getFallbackLanguages();
|
||||
} else {
|
||||
$languages = (array)$this->getDefault();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$languages = ['en'];
|
||||
}
|
||||
|
||||
foreach ((array)$languages as $lang) {
|
||||
$translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
|
||||
if ($translation_array && array_key_exists($index, $translation_array)) {
|
||||
return $translation_array[$index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($html_out) {
|
||||
return '<span class="untranslated">' . $key . '[' . $index . ']</span>';
|
||||
}
|
||||
|
||||
return $key . '[' . $index . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the translation text for a given lang and key
|
||||
*
|
||||
* @param string $lang lang code
|
||||
* @param string $key key to lookup with
|
||||
* @param bool $array_support
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTranslation($lang, $key, $array_support = false)
|
||||
{
|
||||
$translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
|
||||
if (!$array_support && is_array($translation)) {
|
||||
return (string)array_shift($translation);
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the browser accepted languages
|
||||
*
|
||||
* @param array $accept_langs
|
||||
* @return array
|
||||
* @deprecated 1.6 No longer used - using content negotiation.
|
||||
*/
|
||||
public function getBrowserLanguages($accept_langs = [])
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED);
|
||||
|
||||
if (empty($this->http_accept_language)) {
|
||||
if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
$accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
|
||||
} else {
|
||||
return $accept_langs;
|
||||
}
|
||||
|
||||
$langs = [];
|
||||
|
||||
foreach (explode(',', $accept_langs) as $k => $pref) {
|
||||
// split $pref again by ';q='
|
||||
// and decorate the language entries by inverted position
|
||||
if (false !== ($i = strpos($pref, ';q='))) {
|
||||
$langs[substr($pref, 0, $i)] = [(float)substr($pref, $i + 3), -$k];
|
||||
} else {
|
||||
$langs[$pref] = [1, -$k];
|
||||
}
|
||||
}
|
||||
arsort($langs);
|
||||
|
||||
// no need to undecorate, because we're only interested in the keys
|
||||
$this->http_accept_language = array_keys($langs);
|
||||
}
|
||||
return $this->http_accept_language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessible wrapper to LanguageCodes
|
||||
*
|
||||
* @param string $code
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function getLanguageCode($code, $type = 'name')
|
||||
{
|
||||
return LanguageCodes::get($code, $type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Language
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Language;
|
||||
|
||||
class LanguageCodes
|
||||
{
|
||||
protected static $codes = [
|
||||
'af' => [ 'name' => 'Afrikaans', 'nativeName' => 'Afrikaans' ],
|
||||
'ak' => [ 'name' => 'Akan', 'nativeName' => 'Akan' ], // unverified native name
|
||||
'ast' => [ 'name' => 'Asturian', 'nativeName' => 'Asturianu' ],
|
||||
'ar' => [ 'name' => 'Arabic', 'nativeName' => 'عربي', 'orientation' => 'rtl'],
|
||||
'as' => [ 'name' => 'Assamese', 'nativeName' => 'অসমীয়া' ],
|
||||
'be' => [ 'name' => 'Belarusian', 'nativeName' => 'Беларуская' ],
|
||||
'bg' => [ 'name' => 'Bulgarian', 'nativeName' => 'Български' ],
|
||||
'bn' => [ 'name' => 'Bengali', 'nativeName' => 'বাংলা' ],
|
||||
'bn-BD' => [ 'name' => 'Bengali (Bangladesh)', 'nativeName' => 'বাংলা (বাংলাদেশ)' ],
|
||||
'bn-IN' => [ 'name' => 'Bengali (India)', 'nativeName' => 'বাংলা (ভারত)' ],
|
||||
'br' => [ 'name' => 'Breton', 'nativeName' => 'Brezhoneg' ],
|
||||
'bs' => [ 'name' => 'Bosnian', 'nativeName' => 'Bosanski' ],
|
||||
'ca' => [ 'name' => 'Catalan', 'nativeName' => 'Català' ],
|
||||
'ca-valencia'=> [ 'name' => 'Catalan (Valencian)', 'nativeName' => 'Català (valencià)' ], // not iso-639-1. a=l10n-drivers
|
||||
'cs' => [ 'name' => 'Czech', 'nativeName' => 'Čeština' ],
|
||||
'cy' => [ 'name' => 'Welsh', 'nativeName' => 'Cymraeg' ],
|
||||
'da' => [ 'name' => 'Danish', 'nativeName' => 'Dansk' ],
|
||||
'de' => [ 'name' => 'German', 'nativeName' => 'Deutsch' ],
|
||||
'de-AT' => [ 'name' => 'German (Austria)', 'nativeName' => 'Deutsch (Österreich)' ],
|
||||
'de-CH' => [ 'name' => 'German (Switzerland)', 'nativeName' => 'Deutsch (Schweiz)' ],
|
||||
'de-DE' => [ 'name' => 'German (Germany)', 'nativeName' => 'Deutsch (Deutschland)' ],
|
||||
'dsb' => [ 'name' => 'Lower Sorbian', 'nativeName' => 'Dolnoserbšćina' ], // iso-639-2
|
||||
'el' => [ 'name' => 'Greek', 'nativeName' => 'Ελληνικά' ],
|
||||
'en' => [ 'name' => 'English', 'nativeName' => 'English' ],
|
||||
'en-AU' => [ 'name' => 'English (Australian)', 'nativeName' => 'English (Australian)' ],
|
||||
'en-CA' => [ 'name' => 'English (Canadian)', 'nativeName' => 'English (Canadian)' ],
|
||||
'en-GB' => [ 'name' => 'English (British)', 'nativeName' => 'English (British)' ],
|
||||
'en-NZ' => [ 'name' => 'English (New Zealand)', 'nativeName' => 'English (New Zealand)' ],
|
||||
'en-US' => [ 'name' => 'English (US)', 'nativeName' => 'English (US)' ],
|
||||
'en-ZA' => [ 'name' => 'English (South African)', 'nativeName' => 'English (South African)' ],
|
||||
'eo' => [ 'name' => 'Esperanto', 'nativeName' => 'Esperanto' ],
|
||||
'es' => [ 'name' => 'Spanish', 'nativeName' => 'Español' ],
|
||||
'es-AR' => [ 'name' => 'Spanish (Argentina)', 'nativeName' => 'Español (de Argentina)' ],
|
||||
'es-CL' => [ 'name' => 'Spanish (Chile)', 'nativeName' => 'Español (de Chile)' ],
|
||||
'es-ES' => [ 'name' => 'Spanish (Spain)', 'nativeName' => 'Español (de España)' ],
|
||||
'es-MX' => [ 'name' => 'Spanish (Mexico)', 'nativeName' => 'Español (de México)' ],
|
||||
'et' => [ 'name' => 'Estonian', 'nativeName' => 'Eesti keel' ],
|
||||
'eu' => [ 'name' => 'Basque', 'nativeName' => 'Euskara' ],
|
||||
'fa' => [ 'name' => 'Persian', 'nativeName' => 'فارسی' , 'orientation' => 'rtl' ],
|
||||
'fi' => [ 'name' => 'Finnish', 'nativeName' => 'Suomi' ],
|
||||
'fj-FJ' => [ 'name' => 'Fijian', 'nativeName' => 'Vosa vaka-Viti' ],
|
||||
'fr' => [ 'name' => 'French', 'nativeName' => 'Français' ],
|
||||
'fr-CA' => [ 'name' => 'French (Canada)', 'nativeName' => 'Français (Canada)' ],
|
||||
'fr-FR' => [ 'name' => 'French (France)', 'nativeName' => 'Français (France)' ],
|
||||
'fur' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ],
|
||||
'fur-IT' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ],
|
||||
'fy' => [ 'name' => 'Frisian', 'nativeName' => 'Frysk' ],
|
||||
'fy-NL' => [ 'name' => 'Frisian', 'nativeName' => 'Frysk' ],
|
||||
'ga' => [ 'name' => 'Irish', 'nativeName' => 'Gaeilge' ],
|
||||
'ga-IE' => [ 'name' => 'Irish (Ireland)', 'nativeName' => 'Gaeilge (Éire)' ],
|
||||
'gd' => [ 'name' => 'Gaelic (Scotland)', 'nativeName' => 'Gàidhlig' ],
|
||||
'gl' => [ 'name' => 'Galician', 'nativeName' => 'Galego' ],
|
||||
'gu' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ],
|
||||
'gu-IN' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ],
|
||||
'he' => [ 'name' => 'Hebrew', 'nativeName' => 'עברית', 'orientation' => 'rtl' ],
|
||||
'hi' => [ 'name' => 'Hindi', 'nativeName' => 'हिन्दी' ],
|
||||
'hi-IN' => [ 'name' => 'Hindi (India)', 'nativeName' => 'हिन्दी (भारत)' ],
|
||||
'hr' => [ 'name' => 'Croatian', 'nativeName' => 'Hrvatski' ],
|
||||
'hsb' => [ 'name' => 'Upper Sorbian', 'nativeName' => 'Hornjoserbsce' ],
|
||||
'hu' => [ 'name' => 'Hungarian', 'nativeName' => 'Magyar' ],
|
||||
'hy' => [ 'name' => 'Armenian', 'nativeName' => 'Հայերեն' ],
|
||||
'hy-AM' => [ 'name' => 'Armenian', 'nativeName' => 'Հայերեն' ],
|
||||
'id' => [ 'name' => 'Indonesian', 'nativeName' => 'Bahasa Indonesia' ],
|
||||
'is' => [ 'name' => 'Icelandic', 'nativeName' => 'íslenska' ],
|
||||
'it' => [ 'name' => 'Italian', 'nativeName' => 'Italiano' ],
|
||||
'ja' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ],
|
||||
'ja-JP' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ], // not iso-639-1
|
||||
'ka' => [ 'name' => 'Georgian', 'nativeName' => 'ქართული' ],
|
||||
'kk' => [ 'name' => 'Kazakh', 'nativeName' => 'Қазақ' ],
|
||||
'kn' => [ 'name' => 'Kannada', 'nativeName' => 'ಕನ್ನಡ' ],
|
||||
'ko' => [ 'name' => 'Korean', 'nativeName' => '한국어' ],
|
||||
'ku' => [ 'name' => 'Kurdish', 'nativeName' => 'Kurdî' ],
|
||||
'la' => [ 'name' => 'Latin', 'nativeName' => 'Latina' ],
|
||||
'lb' => [ 'name' => 'Luxembourgish', 'nativeName' => 'Lëtzebuergesch' ],
|
||||
'lg' => [ 'name' => 'Luganda', 'nativeName' => 'Luganda' ],
|
||||
'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių kalba' ],
|
||||
'lv' => [ 'name' => 'Latvian', 'nativeName' => 'Latviešu' ],
|
||||
'mai' => [ 'name' => 'Maithili', 'nativeName' => 'मैथिली মৈথিলী' ],
|
||||
'mg' => [ 'name' => 'Malagasy', 'nativeName' => 'Malagasy' ],
|
||||
'mi' => [ 'name' => 'Maori (Aotearoa)', 'nativeName' => 'Māori (Aotearoa)' ],
|
||||
'mk' => [ 'name' => 'Macedonian', 'nativeName' => 'Македонски' ],
|
||||
'ml' => [ 'name' => 'Malayalam', 'nativeName' => 'മലയാളം' ],
|
||||
'mn' => [ 'name' => 'Mongolian', 'nativeName' => 'Монгол' ],
|
||||
'mr' => [ 'name' => 'Marathi', 'nativeName' => 'मराठी' ],
|
||||
'no' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ],
|
||||
'nb' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ],
|
||||
'nb-NO' => [ 'name' => 'Norwegian (Bokmål)', 'nativeName' => 'Norsk bokmål' ],
|
||||
'ne-NP' => [ 'name' => 'Nepali', 'nativeName' => 'नेपाली' ],
|
||||
'nn-NO' => [ 'name' => 'Norwegian (Nynorsk)', 'nativeName' => 'Norsk nynorsk' ],
|
||||
'nl' => [ 'name' => 'Dutch', 'nativeName' => 'Nederlands' ],
|
||||
'nr' => [ 'name' => 'Ndebele, South', 'nativeName' => 'IsiNdebele' ],
|
||||
'nso' => [ 'name' => 'Northern Sotho', 'nativeName' => 'Sepedi' ],
|
||||
'oc' => [ 'name' => 'Occitan (Lengadocian)', 'nativeName' => 'Occitan (lengadocian)' ],
|
||||
'or' => [ 'name' => 'Oriya', 'nativeName' => 'ଓଡ଼ିଆ' ],
|
||||
'pa' => [ 'name' => 'Punjabi', 'nativeName' => 'ਪੰਜਾਬੀ' ],
|
||||
'pa-IN' => [ 'name' => 'Punjabi', 'nativeName' => 'ਪੰਜਾਬੀ' ],
|
||||
'pl' => [ 'name' => 'Polish', 'nativeName' => 'Polski' ],
|
||||
'pt' => [ 'name' => 'Portuguese', 'nativeName' => 'Português' ],
|
||||
'pt-BR' => [ 'name' => 'Portuguese (Brazilian)', 'nativeName' => 'Português (do Brasil)' ],
|
||||
'pt-PT' => [ 'name' => 'Portuguese (Portugal)', 'nativeName' => 'Português (Europeu)' ],
|
||||
'ro' => [ 'name' => 'Romanian', 'nativeName' => 'Română' ],
|
||||
'rm' => [ 'name' => 'Romansh', 'nativeName' => 'Rumantsch' ],
|
||||
'ru' => [ 'name' => 'Russian', 'nativeName' => 'Русский' ],
|
||||
'rw' => [ 'name' => 'Kinyarwanda', 'nativeName' => 'Ikinyarwanda' ],
|
||||
'si' => [ 'name' => 'Sinhala', 'nativeName' => 'සිංහල' ],
|
||||
'sk' => [ 'name' => 'Slovak', 'nativeName' => 'Slovenčina' ],
|
||||
'sl' => [ 'name' => 'Slovenian', 'nativeName' => 'Slovensko' ],
|
||||
'son' => [ 'name' => 'Songhai', 'nativeName' => 'Soŋay' ],
|
||||
'sq' => [ 'name' => 'Albanian', 'nativeName' => 'Shqip' ],
|
||||
'sr' => [ 'name' => 'Serbian', 'nativeName' => 'Српски' ],
|
||||
'sr-Latn' => [ 'name' => 'Serbian', 'nativeName' => 'Srpski' ], // follows RFC 4646
|
||||
'ss' => [ 'name' => 'Siswati', 'nativeName' => 'siSwati' ],
|
||||
'st' => [ 'name' => 'Southern Sotho', 'nativeName' => 'Sesotho' ],
|
||||
'sv' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ],
|
||||
'sv-SE' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ],
|
||||
'ta' => [ 'name' => 'Tamil', 'nativeName' => 'தமிழ்' ],
|
||||
'ta-IN' => [ 'name' => 'Tamil (India)', 'nativeName' => 'தமிழ் (இந்தியா)' ],
|
||||
'ta-LK' => [ 'name' => 'Tamil (Sri Lanka)', 'nativeName' => 'தமிழ் (இலங்கை)' ],
|
||||
'te' => [ 'name' => 'Telugu', 'nativeName' => 'తెలుగు' ],
|
||||
'th' => [ 'name' => 'Thai', 'nativeName' => 'ไทย' ],
|
||||
'tlh' => [ 'name' => 'Klingon', 'nativeName' => 'Klingon' ],
|
||||
'tn' => [ 'name' => 'Tswana', 'nativeName' => 'Setswana' ],
|
||||
'tr' => [ 'name' => 'Turkish', 'nativeName' => 'Türkçe' ],
|
||||
'ts' => [ 'name' => 'Tsonga', 'nativeName' => 'Xitsonga' ],
|
||||
'tt' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ],
|
||||
'tt-RU' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ],
|
||||
'uk' => [ 'name' => 'Ukrainian', 'nativeName' => 'Українська' ],
|
||||
'ur' => [ 'name' => 'Urdu', 'nativeName' => 'اُردو', 'orientation' => 'rtl' ],
|
||||
've' => [ 'name' => 'Venda', 'nativeName' => 'Tshivenḓa' ],
|
||||
'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ],
|
||||
'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ],
|
||||
'xh' => [ 'name' => 'Xhosa', 'nativeName' => 'isiXhosa' ],
|
||||
'zh' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ],
|
||||
'zh-CN' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ],
|
||||
'zh-TW' => [ 'name' => 'Chinese (Traditional)', 'nativeName' => '正體中文 (繁體)' ],
|
||||
'zu' => [ 'name' => 'Zulu', 'nativeName' => 'isiZulu' ]
|
||||
];
|
||||
|
||||
public static function getName($code)
|
||||
{
|
||||
return static::get($code, 'name');
|
||||
}
|
||||
|
||||
public static function getNativeName($code)
|
||||
{
|
||||
if (isset(static::$codes[$code])) {
|
||||
return static::get($code, 'nativeName');
|
||||
}
|
||||
|
||||
if (preg_match('/[a-zA-Z]{2}-[a-zA-Z]{2}/', $code)) {
|
||||
return static::get(substr($code, 0, 2), 'nativeName') . ' (' . substr($code, -2) . ')';
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
public static function getOrientation($code)
|
||||
{
|
||||
if (isset(static::$codes[$code])) {
|
||||
if (isset(static::$codes[$code]['orientation'])) {
|
||||
return static::get($code, 'orientation');
|
||||
}
|
||||
}
|
||||
return 'ltr';
|
||||
}
|
||||
|
||||
public static function isRtl($code)
|
||||
{
|
||||
return static::getOrientation($code) === 'rtl';
|
||||
}
|
||||
|
||||
public static function getNames(array $keys)
|
||||
{
|
||||
$results = [];
|
||||
foreach ($keys as $key) {
|
||||
if (isset(static::$codes[$key])) {
|
||||
$results[$key] = static::$codes[$key];
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public static function get($code, $type)
|
||||
{
|
||||
if (isset(static::$codes[$code][$type])) {
|
||||
return static::$codes[$code][$type];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Markdown
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts;
|
||||
|
||||
class Parsedown extends \Parsedown
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
/**
|
||||
* Parsedown constructor.
|
||||
*
|
||||
* @param Excerpts|null $excerpts
|
||||
* @param array|null $defaults
|
||||
*/
|
||||
public function __construct($excerpts = null, $defaults = null)
|
||||
{
|
||||
if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
|
||||
// Deprecated in Grav 1.6.10
|
||||
if ($defaults) {
|
||||
$defaults = ['markdown' => $defaults];
|
||||
}
|
||||
$excerpts = new Excerpts($excerpts, $defaults);
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
$this->init($excerpts, $defaults);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Markdown
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts;
|
||||
|
||||
class ParsedownExtra extends \ParsedownExtra
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
/**
|
||||
* ParsedownExtra constructor.
|
||||
*
|
||||
* @param Excerpts|null $excerpts
|
||||
* @param array|null $defaults
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($excerpts = null, $defaults = null)
|
||||
{
|
||||
if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
|
||||
// Deprecated in Grav 1.6.10
|
||||
if ($defaults) {
|
||||
$defaults = ['markdown' => $defaults];
|
||||
}
|
||||
$excerpts = new Excerpts($excerpts, $defaults);
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->init($excerpts, $defaults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Markdown
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
use Grav\Common\Page\Markdown\Excerpts;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
|
||||
trait ParsedownGravTrait
|
||||
{
|
||||
/** @var Excerpts */
|
||||
protected $excerpts;
|
||||
|
||||
protected $special_chars;
|
||||
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
|
||||
|
||||
public $completable_blocks = [];
|
||||
public $continuable_blocks = [];
|
||||
|
||||
/**
|
||||
* Initialization function to setup key variables needed by the MarkdownGravLinkTrait
|
||||
*
|
||||
* @param PageInterface|Excerpts|null $excerpts
|
||||
* @param array|null $defaults
|
||||
*/
|
||||
protected function init($excerpts = null, $defaults = null)
|
||||
{
|
||||
if (!$excerpts || $excerpts instanceof PageInterface) {
|
||||
// Deprecated in Grav 1.6.10
|
||||
if ($defaults) {
|
||||
$defaults = ['markdown' => $defaults];
|
||||
}
|
||||
$this->excerpts = new Excerpts($excerpts, $defaults);
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
|
||||
} else {
|
||||
$this->excerpts = $excerpts;
|
||||
}
|
||||
|
||||
$this->BlockTypes['{'][] = 'TwigTag';
|
||||
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
|
||||
|
||||
$defaults = $this->excerpts->getConfig();
|
||||
|
||||
if (isset($defaults['markdown']['auto_line_breaks'])) {
|
||||
$this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']);
|
||||
}
|
||||
if (isset($defaults['markdown']['auto_url_links'])) {
|
||||
$this->setUrlsLinked($defaults['markdown']['auto_url_links']);
|
||||
}
|
||||
if (isset($defaults['markdown']['escape_markup'])) {
|
||||
$this->setMarkupEscaped($defaults['markdown']['escape_markup']);
|
||||
}
|
||||
if (isset($defaults['markdown']['special_chars'])) {
|
||||
$this->setSpecialChars($defaults['markdown']['special_chars']);
|
||||
}
|
||||
|
||||
$this->excerpts->fireInitializedEvent($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Excerpts
|
||||
*/
|
||||
public function getExcerpts()
|
||||
{
|
||||
return $this->excerpts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Be able to define a new Block type or override an existing one
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $tag
|
||||
* @param bool $continuable
|
||||
* @param bool $completable
|
||||
* @param int|null $index
|
||||
*/
|
||||
public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null)
|
||||
{
|
||||
$block = &$this->unmarkedBlockTypes;
|
||||
if ($type) {
|
||||
if (!isset($this->BlockTypes[$type])) {
|
||||
$this->BlockTypes[$type] = [];
|
||||
}
|
||||
$block = &$this->BlockTypes[$type];
|
||||
}
|
||||
|
||||
if (null === $index) {
|
||||
$block[] = $tag;
|
||||
} else {
|
||||
array_splice($block, $index, 0, [$tag]);
|
||||
}
|
||||
|
||||
if ($continuable) {
|
||||
$this->continuable_blocks[] = $tag;
|
||||
}
|
||||
if ($completable) {
|
||||
$this->completable_blocks[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Be able to define a new Inline type or override an existing one
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $tag
|
||||
* @param int|null $index
|
||||
*/
|
||||
public function addInlineType($type, $tag, $index = null)
|
||||
{
|
||||
if (null === $index || !isset($this->InlineTypes[$type])) {
|
||||
$this->InlineTypes[$type] [] = $tag;
|
||||
} else {
|
||||
array_splice($this->InlineTypes[$type], $index, 0, [$tag]);
|
||||
}
|
||||
|
||||
if (strpos($this->inlineMarkerList, $type) === false) {
|
||||
$this->inlineMarkerList .= $type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default behavior to allow for plugin-provided blocks to be continuable
|
||||
*
|
||||
* @param string $Type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBlockContinuable($Type)
|
||||
{
|
||||
$continuable = \in_array($Type, $this->continuable_blocks, true)
|
||||
|| method_exists($this, 'block' . $Type . 'Continue');
|
||||
|
||||
return $continuable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default behavior to allow for plugin-provided blocks to be completable
|
||||
*
|
||||
* @param string $Type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBlockCompletable($Type)
|
||||
{
|
||||
$completable = \in_array($Type, $this->completable_blocks, true)
|
||||
|| method_exists($this, 'block' . $Type . 'Complete');
|
||||
|
||||
return $completable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make the element function publicly accessible, Medium uses this to render from Twig
|
||||
*
|
||||
* @param array $Element
|
||||
*
|
||||
* @return string markup
|
||||
*/
|
||||
public function elementToHtml(array $Element)
|
||||
{
|
||||
return $this->element($Element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for special chars
|
||||
*
|
||||
* @param array $special_chars
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSpecialChars($special_chars)
|
||||
{
|
||||
$this->special_chars = $special_chars;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Twig tags are treated as block level items with no <p></p> tags
|
||||
*
|
||||
* @param array $line
|
||||
* @return array|null
|
||||
*/
|
||||
protected function blockTwigTag($line)
|
||||
{
|
||||
if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $line['body'], $matches)) {
|
||||
return ['markup' => $line['body']];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function inlineSpecialCharacter($excerpt)
|
||||
{
|
||||
if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $excerpt['text'])) {
|
||||
return [
|
||||
'markup' => '&',
|
||||
'extent' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->special_chars[$excerpt['text'][0]])) {
|
||||
return [
|
||||
'markup' => '&' . $this->special_chars[$excerpt['text'][0]] . ';',
|
||||
'extent' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function inlineImage($excerpt)
|
||||
{
|
||||
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
|
||||
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
|
||||
$excerpt = parent::inlineImage($excerpt);
|
||||
$excerpt['element']['attributes']['src'] = $matches[1];
|
||||
$excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
$excerpt['type'] = 'image';
|
||||
$excerpt = parent::inlineImage($excerpt);
|
||||
|
||||
// if this is an image process it
|
||||
if (isset($excerpt['element']['attributes']['src'])) {
|
||||
$excerpt = $this->excerpts->processImageExcerpt($excerpt);
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
protected function inlineLink($excerpt)
|
||||
{
|
||||
$type = $excerpt['type'] ?? 'link';
|
||||
|
||||
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there
|
||||
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
|
||||
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
|
||||
$excerpt = parent::inlineLink($excerpt);
|
||||
$excerpt['element']['attributes']['href'] = $matches[1];
|
||||
$excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
$excerpt = parent::inlineLink($excerpt);
|
||||
|
||||
// if this is a link
|
||||
if (isset($excerpt['element']['attributes']['href'])) {
|
||||
$excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type);
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* For extending this class via plugins
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if (isset($this->{$method}) === true) {
|
||||
$func = $this->{$method};
|
||||
|
||||
return \call_user_func_array($func, $args);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Media
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media collection interface.
|
||||
*/
|
||||
interface MediaCollectionInterface extends \Grav\Framework\Media\Interfaces\MediaCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Return media path.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPath();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Media
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media interface.
|
||||
*/
|
||||
interface MediaInterface extends \Grav\Framework\Media\Interfaces\MediaInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Media
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media object interface.
|
||||
*/
|
||||
interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObjectInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Media
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Media\Traits;
|
||||
|
||||
use Grav\Common\Cache;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
||||
use Grav\Common\Page\Media;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
trait MediaTrait
|
||||
{
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Get filesystem path to the associated media.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
abstract public function getMediaFolder();
|
||||
|
||||
/**
|
||||
* Get display order for the associated media.
|
||||
*
|
||||
* @return array Empty array means default ordering.
|
||||
*/
|
||||
public function getMediaOrder()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URI ot the associated media. Method will return null if path isn't URI.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getMediaUri()
|
||||
{
|
||||
$folder = $this->getMediaFolder();
|
||||
|
||||
if (strpos($folder, '://')) {
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$user = $locator->findResource('user://');
|
||||
if (strpos($folder, $user) === 0) {
|
||||
return 'user://' . substr($folder, \strlen($user)+1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the associated media collection.
|
||||
*
|
||||
* @return MediaCollectionInterface Representation of associated media.
|
||||
*/
|
||||
public function getMedia()
|
||||
{
|
||||
if ($this->media === null) {
|
||||
$cache = $this->getMediaCache();
|
||||
|
||||
// Use cached media if possible.
|
||||
$cacheKey = md5('media' . $this->getCacheKey());
|
||||
if (!$media = $cache->get($cacheKey)) {
|
||||
$media = new Media($this->getMediaFolder(), $this->getMediaOrder());
|
||||
$cache->set($cacheKey, $media);
|
||||
}
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
return $this->media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the associated media collection.
|
||||
*
|
||||
* @param MediaCollectionInterface $media Representation of associated media.
|
||||
* @return $this
|
||||
*/
|
||||
protected function setMedia(MediaCollectionInterface $media)
|
||||
{
|
||||
$cache = $this->getMediaCache();
|
||||
$cacheKey = md5('media' . $this->getCacheKey());
|
||||
$cache->set($cacheKey, $media);
|
||||
|
||||
$this->media = $media;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear media cache.
|
||||
*/
|
||||
protected function clearMediaCache()
|
||||
{
|
||||
$cache = $this->getMediaCache();
|
||||
$cacheKey = md5('media' . $this->getCacheKey());
|
||||
$cache->delete($cacheKey);
|
||||
|
||||
$this->media = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheInterface
|
||||
*/
|
||||
protected function getMediaCache()
|
||||
{
|
||||
/** @var Cache $cache */
|
||||
$cache = Grav::instance()['cache'];
|
||||
|
||||
return $cache->getSimpleCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getCacheKey();
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Iterator;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class Collection extends Iterator
|
||||
{
|
||||
/**
|
||||
* @var Pages
|
||||
*/
|
||||
protected $pages;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params;
|
||||
|
||||
/**
|
||||
* Collection constructor.
|
||||
*
|
||||
* @param array $items
|
||||
* @param array $params
|
||||
* @param Pages|null $pages
|
||||
*/
|
||||
public function __construct($items = [], array $params = [], Pages $pages = null)
|
||||
{
|
||||
parent::__construct($items);
|
||||
|
||||
$this->params = $params;
|
||||
$this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection params
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function params()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single page to a collection
|
||||
*
|
||||
* @param PageInterface $page
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addPage(PageInterface $page)
|
||||
{
|
||||
$this->items[$page->path()] = ['slug' => $page->slug()];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a page with path and slug
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $slug
|
||||
* @return $this
|
||||
*/
|
||||
public function add($path, $slug)
|
||||
{
|
||||
$this->items[$path] = ['slug' => $slug];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Create a copy of this collection
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function copy()
|
||||
{
|
||||
return new static($this->items, $this->params, $this->pages);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Merge another collection with the current collection
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @return $this
|
||||
*/
|
||||
public function merge(Collection $collection)
|
||||
{
|
||||
foreach($collection as $page) {
|
||||
$this->addPage($page);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intersect another collection with the current collection
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @return $this
|
||||
*/
|
||||
public function intersect(Collection $collection)
|
||||
{
|
||||
$array1 = $this->items;
|
||||
$array2 = $collection->toArray();
|
||||
|
||||
$this->items = array_uintersect($array1, $array2, function($val1, $val2) {
|
||||
return strcmp($val1['slug'], $val2['slug']);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parameters to the Collection
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setParams(array $params)
|
||||
{
|
||||
$this->params = array_merge($this->params, $params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current page.
|
||||
*
|
||||
* @return PageInterface
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$current = parent::key();
|
||||
|
||||
return $this->pages->get($current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current slug.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
$current = parent::current();
|
||||
|
||||
return $current['slug'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at specified offset.
|
||||
*
|
||||
* @param mixed $offset The offset to retrieve.
|
||||
*
|
||||
* @return mixed Can return all value types.
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->pages->get($offset) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split collection into array of smaller collections.
|
||||
*
|
||||
* @param int $size
|
||||
* @return array|Collection[]
|
||||
*/
|
||||
public function batch($size)
|
||||
{
|
||||
$chunks = array_chunk($this->items, $size, true);
|
||||
|
||||
$list = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$list[] = new static($chunk, $this->params, $this->pages);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from the list.
|
||||
*
|
||||
* @param PageInterface|string|null $key
|
||||
*
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function remove($key = null)
|
||||
{
|
||||
if ($key instanceof PageInterface) {
|
||||
$key = $key->path();
|
||||
} elseif (null === $key) {
|
||||
$key = (string)key($this->items);
|
||||
}
|
||||
if (!\is_string($key)) {
|
||||
throw new \InvalidArgumentException('Invalid argument $key.');
|
||||
}
|
||||
|
||||
parent::remove($key);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder collection.
|
||||
*
|
||||
* @param string $by
|
||||
* @param string $dir
|
||||
* @param array $manual
|
||||
* @param string $sort_flags
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
|
||||
{
|
||||
$this->items = $this->pages->sortCollection($this, $by, $dir, $manual, $sort_flags);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return $this->items && $path === array_keys($this->items)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return $this->items && $path === array_keys($this->items)[\count($this->items) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return PageInterface The previous item.
|
||||
*/
|
||||
public function prevSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return PageInterface The next item.
|
||||
*/
|
||||
public function nextSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adjacent sibling based on a direction.
|
||||
*
|
||||
* @param string $path
|
||||
* @param int $direction either -1 or +1
|
||||
*
|
||||
* @return PageInterface|Collection The sibling item.
|
||||
*/
|
||||
public function adjacentSibling($path, $direction = 1)
|
||||
{
|
||||
$values = array_keys($this->items);
|
||||
$keys = array_flip($values);
|
||||
|
||||
if (array_key_exists($path, $keys)) {
|
||||
$index = $keys[$path] - $direction;
|
||||
|
||||
return isset($values[$index]) ? $this->offsetGet($values[$index]) : $this;
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @param string $path the path the item
|
||||
*
|
||||
* @return int the index of the current page.
|
||||
*/
|
||||
public function currentPosition($path)
|
||||
{
|
||||
return \array_search($path, \array_keys($this->items), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateRange($startDate, $endDate = false, $field = null)
|
||||
{
|
||||
$start = Utils::date2timestamp($startDate);
|
||||
$end = $endDate ? Utils::date2timestamp($endDate) : false;
|
||||
|
||||
$date_range = [];
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null) {
|
||||
$date = $field ? strtotime($page->value($field)) : $page->date();
|
||||
|
||||
if ($date >= $start && (!$end || $date <= $end)) {
|
||||
$date_range[$path] = $slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $date_range;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only visible pages
|
||||
*
|
||||
* @return Collection The collection with only visible pages
|
||||
*/
|
||||
public function visible()
|
||||
{
|
||||
$visible = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && $page->visible()) {
|
||||
$visible[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-visible pages
|
||||
*
|
||||
* @return Collection The collection with only non-visible pages
|
||||
*/
|
||||
public function nonVisible()
|
||||
{
|
||||
$visible = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && !$page->visible()) {
|
||||
$visible[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only modular pages
|
||||
*
|
||||
* @return Collection The collection with only modular pages
|
||||
*/
|
||||
public function modular()
|
||||
{
|
||||
$modular = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && $page->modular()) {
|
||||
$modular[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $modular;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-modular pages
|
||||
*
|
||||
* @return Collection The collection with only non-modular pages
|
||||
*/
|
||||
public function nonModular()
|
||||
{
|
||||
$modular = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && !$page->modular()) {
|
||||
$modular[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $modular;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only published pages
|
||||
*
|
||||
* @return Collection The collection with only published pages
|
||||
*/
|
||||
public function published()
|
||||
{
|
||||
$published = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && $page->published()) {
|
||||
$published[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $published;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-published pages
|
||||
*
|
||||
* @return Collection The collection with only non-published pages
|
||||
*/
|
||||
public function nonPublished()
|
||||
{
|
||||
$published = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && !$page->published()) {
|
||||
$published[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $published;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only routable pages
|
||||
*
|
||||
* @return Collection The collection with only routable pages
|
||||
*/
|
||||
public function routable()
|
||||
{
|
||||
$routable = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
|
||||
if ($page !== null && $page->routable()) {
|
||||
$routable[$path] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $routable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only non-routable pages
|
||||
*
|
||||
* @return Collection The collection with only non-routable pages
|
||||
*/
|
||||
public function nonRoutable()
|
||||
{
|
||||
$routable = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && !$page->routable()) {
|
||||
$routable[$path] = $slug;
|
||||
}
|
||||
}
|
||||
$this->items = $routable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of the specified type
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return Collection The collection
|
||||
*/
|
||||
public function ofType($type)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && $page->template() === $type) {
|
||||
$items[$path] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $items;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of one of the specified types
|
||||
*
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return Collection The collection
|
||||
*/
|
||||
public function ofOneOfTheseTypes($types)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page !== null && \in_array($page->template(), $types, true)) {
|
||||
$items[$path] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $items;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new collection with only pages of one of the specified access levels
|
||||
*
|
||||
* @param array $accessLevels
|
||||
*
|
||||
* @return Collection The collection
|
||||
*/
|
||||
public function ofOneOfTheseAccessLevels($accessLevels)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
|
||||
if ($page !== null && isset($page->header()->access)) {
|
||||
if (\is_array($page->header()->access)) {
|
||||
//Multiple values for access
|
||||
$valid = false;
|
||||
|
||||
foreach ($page->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) {
|
||||
$items[$path] = $slug;
|
||||
}
|
||||
} else {
|
||||
//Single value for access
|
||||
if (\in_array($page->header()->access, $accessLevels)) {
|
||||
$items[$path] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $items;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extended version of this Collection with each page keyed by route
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function toExtendedArray()
|
||||
{
|
||||
$items = [];
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
|
||||
if ($page !== null) {
|
||||
$items[$page->route()] = $page->toArray();
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use RocketTheme\Toolbox\ArrayTraits\Constructor;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccess;
|
||||
|
||||
class Header implements \ArrayAccess
|
||||
{
|
||||
use NestedArrayAccess, Constructor;
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Interfaces;
|
||||
|
||||
use Grav\Common\Page\Media;
|
||||
|
||||
/**
|
||||
* Methods currently implemented in Flex Page emulation layer.
|
||||
*/
|
||||
interface PageContentInterface
|
||||
{
|
||||
/**
|
||||
* Gets and Sets the header based on the YAML configuration at the top of the .md file
|
||||
*
|
||||
* @param object|array $var a YAML object representing the configuration for the file
|
||||
*
|
||||
* @return object the current YAML configuration
|
||||
*/
|
||||
public function header($var = null);
|
||||
|
||||
/**
|
||||
* Get the summary.
|
||||
*
|
||||
* @param int $size Max summary size.
|
||||
*
|
||||
* @param bool $textOnly Only count text size.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summary($size = null, $textOnly = false);
|
||||
|
||||
/**
|
||||
* Gets and Sets the content based on content portion of the .md file
|
||||
*
|
||||
* @param string $var Content
|
||||
*
|
||||
* @return string Content
|
||||
*/
|
||||
public function content($var = null);
|
||||
|
||||
/**
|
||||
* Needed by the onPageContentProcessed event to get the raw page content
|
||||
*
|
||||
* @return string the current page content
|
||||
*/
|
||||
public function getRawContent();
|
||||
|
||||
/**
|
||||
* Needed by the onPageContentProcessed event to set the raw page content
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
public function setRawContent($content);
|
||||
|
||||
/**
|
||||
* Gets and Sets the Page raw content
|
||||
*
|
||||
* @param string|null $var
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function rawMarkdown($var = null);
|
||||
|
||||
/**
|
||||
* Get value from a page variable (used mostly for creating edit forms).
|
||||
*
|
||||
* @param string $name Variable name.
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function value($name, $default = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the associated media as found in the page folder.
|
||||
*
|
||||
* @param Media $var Representation of associated media.
|
||||
*
|
||||
* @return Media Representation of associated media.
|
||||
*/
|
||||
public function media($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name
|
||||
*
|
||||
* @param string $var the title of the Page
|
||||
*
|
||||
* @return string the title of the Page
|
||||
*/
|
||||
public function title($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the menu name for this Page. This is the text that can be used specifically for navigation.
|
||||
* If no menu field is set, it will use the title()
|
||||
*
|
||||
* @param string $var the menu field for the page
|
||||
*
|
||||
* @return string the menu field for the page
|
||||
*/
|
||||
public function menu($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets whether or not this Page is visible for navigation
|
||||
*
|
||||
* @param bool $var true if the page is visible
|
||||
*
|
||||
* @return bool true if the page is visible
|
||||
*/
|
||||
public function visible($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets whether or not this Page is considered published
|
||||
*
|
||||
* @param bool $var true if the page is published
|
||||
*
|
||||
* @return bool true if the page is published
|
||||
*/
|
||||
public function published($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the Page publish date
|
||||
*
|
||||
* @param string $var string representation of a date
|
||||
*
|
||||
* @return int unix timestamp representation of the date
|
||||
*/
|
||||
public function publishDate($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the Page unpublish date
|
||||
*
|
||||
* @param string $var string representation of a date
|
||||
*
|
||||
* @return int|null unix timestamp representation of the date
|
||||
*/
|
||||
public function unpublishDate($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the process setup for this Page. This is multi-dimensional array that consists of
|
||||
* a simple array of arrays with the form array("markdown"=>true) for example
|
||||
*
|
||||
* @param array $var an Array of name value pairs where the name is the process and value is true or false
|
||||
*
|
||||
* @return array an Array of name value pairs where the name is the process and value is true or false
|
||||
*/
|
||||
public function process($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses
|
||||
* the parent folder from the path
|
||||
*
|
||||
* @param string $var the slug, e.g. 'my-blog'
|
||||
*
|
||||
* @return string the slug
|
||||
*/
|
||||
public function slug($var = null);
|
||||
|
||||
/**
|
||||
* Get/set order number of this page.
|
||||
*
|
||||
* @param int $var
|
||||
*
|
||||
* @return int|bool
|
||||
*/
|
||||
public function order($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the identifier for this Page object.
|
||||
*
|
||||
* @param string $var the identifier
|
||||
*
|
||||
* @return string the identifier
|
||||
*/
|
||||
public function id($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the modified timestamp.
|
||||
*
|
||||
* @param int $var modified unix timestamp
|
||||
*
|
||||
* @return int modified unix timestamp
|
||||
*/
|
||||
public function modified($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the option to show the last_modified header for the page.
|
||||
*
|
||||
* @param boolean $var show last_modified header
|
||||
*
|
||||
* @return boolean show last_modified header
|
||||
*/
|
||||
public function lastModified($var = null);
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string $var Optional path
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function folder($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the date for this Page object. This is typically passed in via the page headers
|
||||
*
|
||||
* @param string $var string representation of a date
|
||||
*
|
||||
* @return int unix timestamp representation of the date
|
||||
*/
|
||||
public function date($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the date format for this Page object. This is typically passed in via the page headers
|
||||
* using typical PHP date string structure - http://php.net/manual/en/function.date.php
|
||||
*
|
||||
* @param string $var string representation of a date format
|
||||
*
|
||||
* @return string string representation of a date format
|
||||
*/
|
||||
public function dateformat($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with.
|
||||
*
|
||||
* @param array $var an array of taxonomies
|
||||
*
|
||||
* @return array an array of taxonomies
|
||||
*/
|
||||
public function taxonomy($var = null);
|
||||
|
||||
/**
|
||||
* Gets the configured state of the processing method.
|
||||
*
|
||||
* @param string $process the process, eg "twig" or "markdown"
|
||||
*
|
||||
* @return bool whether or not the processing method is enabled for this Page
|
||||
*/
|
||||
public function shouldProcess($process);
|
||||
|
||||
/**
|
||||
* Returns whether or not this Page object has a .md file associated with it or if its just a directory.
|
||||
*
|
||||
* @return bool True if its a page with a .md file associated
|
||||
*/
|
||||
public function isPage();
|
||||
|
||||
/**
|
||||
* Returns whether or not this Page object is a directory or a page.
|
||||
*
|
||||
* @return bool True if its a directory
|
||||
*/
|
||||
public function isDir();
|
||||
|
||||
/**
|
||||
* Returns whether the page exists in the filesystem.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Interfaces;
|
||||
|
||||
use Grav\Common\Media\Interfaces\MediaInterface;
|
||||
|
||||
/**
|
||||
* Class implements page interface.
|
||||
*/
|
||||
interface PageInterface extends PageContentInterface, PageRoutableInterface, PageTranslateInterface, MediaInterface, PageLegacyInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Interfaces;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Page\Collection;
|
||||
use RocketTheme\Toolbox\File\MarkdownFile;
|
||||
|
||||
interface PageLegacyInterface
|
||||
{
|
||||
/**
|
||||
* Initializes the page instance variables based on a file
|
||||
*
|
||||
* @param \SplFileInfo $file The file information for the .md file that the page represents
|
||||
* @param string $extension
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function init(\SplFileInfo $file, $extension = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the raw data
|
||||
*
|
||||
* @param string $var Raw content string
|
||||
*
|
||||
* @return string Raw content string
|
||||
*/
|
||||
public function raw($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the page frontmatter
|
||||
*
|
||||
* @param string|null $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function frontmatter($var = null);
|
||||
|
||||
/**
|
||||
* Modify a header value directly
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function modifyHeader($key, $value);
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function httpResponseCode();
|
||||
|
||||
public function httpHeaders();
|
||||
|
||||
/**
|
||||
* Sets the summary of the page
|
||||
*
|
||||
* @param string $summary Summary
|
||||
*/
|
||||
public function setSummary($summary);
|
||||
|
||||
/**
|
||||
* Get the contentMeta array and initialize content first if it's not already
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function contentMeta();
|
||||
|
||||
/**
|
||||
* Add an entry to the page's contentMeta array
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
*/
|
||||
public function addContentMeta($name, $value);
|
||||
|
||||
/**
|
||||
* Return the whole contentMeta array as it currently stands
|
||||
*
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getContentMeta($name = null);
|
||||
|
||||
/**
|
||||
* Sets the whole content meta array in one shot
|
||||
*
|
||||
* @param array $content_meta
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function setContentMeta($content_meta);
|
||||
|
||||
/**
|
||||
* Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page
|
||||
*/
|
||||
public function cachePageContent();
|
||||
|
||||
/**
|
||||
* Get file object to the page.
|
||||
*
|
||||
* @return MarkdownFile|null
|
||||
*/
|
||||
public function file();
|
||||
|
||||
/**
|
||||
* Save page if there's a file assigned to it.
|
||||
*
|
||||
* @param bool|mixed $reorder Internal use.
|
||||
*/
|
||||
public function save($reorder = true);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Prepare a copy from the page. Copies also everything that's under the current page.
|
||||
*
|
||||
* Returns a new Page object for the copy.
|
||||
* You need to call $this->save() in order to perform the move.
|
||||
*
|
||||
* @param PageInterface $parent New parent page.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function copy(PageInterface $parent);
|
||||
|
||||
/**
|
||||
* Get blueprints for the page.
|
||||
*
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function blueprints();
|
||||
|
||||
/**
|
||||
* Get the blueprint name for this page. Use the blueprint form field if set
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function blueprintName();
|
||||
|
||||
/**
|
||||
* Validate page header.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function validate();
|
||||
|
||||
/**
|
||||
* Filter page header from illegal contents.
|
||||
*/
|
||||
public function filter();
|
||||
|
||||
/**
|
||||
* Get unknown header variables.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extra();
|
||||
|
||||
/**
|
||||
* Convert page to an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray();
|
||||
|
||||
/**
|
||||
* Convert page to YAML encoded string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toYaml();
|
||||
|
||||
/**
|
||||
* Convert page to JSON encoded string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson();
|
||||
|
||||
/**
|
||||
* Returns normalized list of name => form pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function forms();
|
||||
|
||||
/**
|
||||
* @param array $new
|
||||
*/
|
||||
public function addForms(array $new);
|
||||
|
||||
/**
|
||||
* Gets and sets the name field. If no name field is set, it will return 'default.md'.
|
||||
*
|
||||
* @param string $var The name of this page.
|
||||
*
|
||||
* @return string The name of this page.
|
||||
*/
|
||||
public function name($var = null);
|
||||
|
||||
/**
|
||||
* Returns child page type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function childType();
|
||||
|
||||
/**
|
||||
* Gets and sets the template field. This is used to find the correct Twig template file to render.
|
||||
* If no field is set, it will return the name without the .md extension
|
||||
*
|
||||
* @param string $var the template name
|
||||
*
|
||||
* @return string the template name
|
||||
*/
|
||||
public function template($var = null);
|
||||
|
||||
/**
|
||||
* Allows a page to override the output render format, usually the extension provided
|
||||
* in the URL. (e.g. `html`, `json`, `xml`, etc).
|
||||
*
|
||||
* @param null $var
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function templateFormat($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the extension field.
|
||||
*
|
||||
* @param null $var
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function extension($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the expires field. If not set will return the default
|
||||
*
|
||||
* @param int $var The new expires value.
|
||||
*
|
||||
* @return int The expires value
|
||||
*/
|
||||
public function expires($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the cache-control property. If not set it will return the default value (null)
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options
|
||||
*
|
||||
* @param null $var
|
||||
* @return null
|
||||
*/
|
||||
public function cacheControl($var = null);
|
||||
|
||||
public function ssl($var = null);
|
||||
|
||||
/**
|
||||
* Returns the state of the debugger override etting for this page
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function debugger();
|
||||
|
||||
/**
|
||||
* Function to merge page metadata tags and build an array of Metadata objects
|
||||
* that can then be rendered in the page.
|
||||
*
|
||||
* @param array $var an Array of metadata values to set
|
||||
*
|
||||
* @return array an Array of metadata values for the page
|
||||
*/
|
||||
public function metadata($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the option to show the etag header for the page.
|
||||
*
|
||||
* @param bool $var show etag header
|
||||
*
|
||||
* @return bool show etag header
|
||||
*/
|
||||
public function eTag($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the path to the .md file for this Page object.
|
||||
*
|
||||
* @param string $var the file path
|
||||
*
|
||||
* @return string|null the file path
|
||||
*/
|
||||
public function filePath($var = null);
|
||||
|
||||
/**
|
||||
* Gets the relative path to the .md file
|
||||
*
|
||||
* @return string The relative file path
|
||||
*/
|
||||
public function filePathClean();
|
||||
|
||||
/**
|
||||
* Gets and sets the order by which any sub-pages should be sorted.
|
||||
*
|
||||
* @param string $var the order, either "asc" or "desc"
|
||||
*
|
||||
* @return string the order, either "asc" or "desc"
|
||||
* @deprecated 1.6
|
||||
*/
|
||||
public function orderDir($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the order by which the sub-pages should be sorted.
|
||||
*
|
||||
* default - is the order based on the file system, ie 01.Home before 02.Advark
|
||||
* title - is the order based on the title set in the pages
|
||||
* date - is the order based on the date set in the pages
|
||||
* folder - is the order based on the name of the folder with any numerics omitted
|
||||
*
|
||||
* @param string $var supported options include "default", "title", "date", and "folder"
|
||||
*
|
||||
* @return string supported options include "default", "title", "date", and "folder"
|
||||
* @deprecated 1.6
|
||||
*/
|
||||
public function orderBy($var = null);
|
||||
|
||||
/**
|
||||
* Gets the manual order set in the header.
|
||||
*
|
||||
* @param string $var supported options include "default", "title", "date", and "folder"
|
||||
*
|
||||
* @return array
|
||||
* @deprecated 1.6
|
||||
*/
|
||||
public function orderManual($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the maxCount field which describes how many sub-pages should be displayed if the
|
||||
* sub_pages header property is set for this page object.
|
||||
*
|
||||
* @param int $var the maximum number of sub-pages
|
||||
*
|
||||
* @return int the maximum number of sub-pages
|
||||
* @deprecated 1.6
|
||||
*/
|
||||
public function maxCount($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the modular var that helps identify this page is a modular child
|
||||
*
|
||||
* @param bool $var true if modular_twig
|
||||
*
|
||||
* @return bool true if modular_twig
|
||||
*/
|
||||
public function modular($var = null);
|
||||
|
||||
/**
|
||||
* Gets and sets the modular_twig var that helps identify this page as a modular child page that will need
|
||||
* twig processing handled differently from a regular page.
|
||||
*
|
||||
* @param bool $var true if modular_twig
|
||||
*
|
||||
* @return bool true if modular_twig
|
||||
*/
|
||||
public function modularTwig($var = null);
|
||||
|
||||
/**
|
||||
* Returns children of this page.
|
||||
*
|
||||
* @return \Grav\Common\Page\Collection
|
||||
*/
|
||||
public function children();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Gets the previous sibling based on current position.
|
||||
*
|
||||
* @return PageInterface the previous Page item
|
||||
*/
|
||||
public function prevSibling();
|
||||
|
||||
/**
|
||||
* Gets the next sibling based on current position.
|
||||
*
|
||||
* @return PageInterface the next Page item
|
||||
*/
|
||||
public function nextSibling();
|
||||
|
||||
/**
|
||||
* Returns the adjacent sibling based on a direction.
|
||||
*
|
||||
* @param int $direction either -1 or +1
|
||||
*
|
||||
* @return PageInterface|bool the sibling page
|
||||
*/
|
||||
public function adjacentSibling($direction = 1);
|
||||
|
||||
/**
|
||||
* Helper method to return an ancestor page.
|
||||
*
|
||||
* @param bool $lookup Name of the parent folder
|
||||
*
|
||||
* @return PageInterface page you were looking for if it exists
|
||||
*/
|
||||
public function ancestor($lookup = null);
|
||||
|
||||
/**
|
||||
* Helper method to return an ancestor page to inherit from. The current
|
||||
* page object is returned.
|
||||
*
|
||||
* @param string $field Name of the parent folder
|
||||
*
|
||||
* @return PageInterface
|
||||
*/
|
||||
public function inherited($field);
|
||||
|
||||
/**
|
||||
* Helper method to return an ancestor field only to inherit from. The
|
||||
* first occurrence of an ancestor field will be returned if at all.
|
||||
*
|
||||
* @param string $field Name of the parent folder
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function inheritedField($field);
|
||||
|
||||
/**
|
||||
* Helper method to return a page.
|
||||
*
|
||||
* @param string $url the url of the page
|
||||
* @param bool $all
|
||||
*
|
||||
* @return PageInterface page you were looking for if it exists
|
||||
*/
|
||||
public function find($url, $all = false);
|
||||
|
||||
/**
|
||||
* Get a collection of pages in the current context.
|
||||
*
|
||||
* @param string|array $params
|
||||
* @param bool $pagination
|
||||
*
|
||||
* @return Collection
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function collection($params = 'content', $pagination = true);
|
||||
|
||||
/**
|
||||
* @param string|array $value
|
||||
* @param bool $only_published
|
||||
* @return mixed
|
||||
* @internal
|
||||
*/
|
||||
public function evaluate($value, $only_published = true);
|
||||
|
||||
/**
|
||||
* Returns whether or not the current folder exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function folderExists();
|
||||
|
||||
/**
|
||||
* Gets the Page Unmodified (original) version of the page.
|
||||
*
|
||||
* @return PageInterface The original version of the page.
|
||||
*/
|
||||
public function getOriginal();
|
||||
|
||||
/**
|
||||
* Gets the action.
|
||||
*
|
||||
* @return string The Action string.
|
||||
*/
|
||||
public function getAction();
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Interfaces;
|
||||
|
||||
interface PageRoutableInterface
|
||||
{
|
||||
/**
|
||||
* Returns the page extension, got from the page `url_extension` config and falls back to the
|
||||
* system config `system.pages.append_url_extension`.
|
||||
*
|
||||
* @return string The extension of this page. For example `.html`
|
||||
*/
|
||||
public function urlExtension();
|
||||
|
||||
/**
|
||||
* Gets and Sets whether or not this Page is routable, ie you can reach it
|
||||
* via a URL.
|
||||
* The page must be *routable* and *published*
|
||||
*
|
||||
* @param bool $var true if the page is routable
|
||||
*
|
||||
* @return bool true if the page is routable
|
||||
*/
|
||||
public function routable($var = null);
|
||||
|
||||
/**
|
||||
* Gets the URL for a page - alias of url().
|
||||
*
|
||||
* @param bool $include_host
|
||||
*
|
||||
* @return string the permalink
|
||||
*/
|
||||
public function link($include_host = false);
|
||||
|
||||
/**
|
||||
* Gets the URL with host information, aka Permalink.
|
||||
* @return string The permalink.
|
||||
*/
|
||||
public function permalink();
|
||||
|
||||
/**
|
||||
* Returns the canonical URL for a page
|
||||
*
|
||||
* @param bool $include_lang
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function canonical($include_lang = true);
|
||||
|
||||
/**
|
||||
* Gets the url for the Page.
|
||||
*
|
||||
* @param bool $include_host Defaults false, but true would include http://yourhost.com
|
||||
* @param bool $canonical true to return the canonical URL
|
||||
* @param bool $include_lang
|
||||
* @param bool $raw_route
|
||||
*
|
||||
* @return string The url.
|
||||
*/
|
||||
public function url($include_host = false, $canonical = false, $include_lang = true, $raw_route = false);
|
||||
|
||||
/**
|
||||
* Gets the route for the page based on the route headers if available, else from
|
||||
* the parents route and the current Page's slug.
|
||||
*
|
||||
* @param string $var Set new default route.
|
||||
*
|
||||
* @return string The route for the Page.
|
||||
*/
|
||||
public function route($var = null);
|
||||
|
||||
/**
|
||||
* Helper method to clear the route out so it regenerates next time you use it
|
||||
*/
|
||||
public function unsetRouteSlug();
|
||||
|
||||
/**
|
||||
* Gets and Sets the page raw route
|
||||
*
|
||||
* @param string|null $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawRoute($var = null);
|
||||
|
||||
/**
|
||||
* Gets the route aliases for the page based on page headers.
|
||||
*
|
||||
* @param array $var list of route aliases
|
||||
*
|
||||
* @return array The route aliases for the Page.
|
||||
*/
|
||||
public function routeAliases($var = null);
|
||||
|
||||
/**
|
||||
* Gets the canonical route for this page if its set. If provided it will use
|
||||
* that value, else if it's `true` it will use the default route.
|
||||
*
|
||||
* @param string|null $var
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function routeCanonical($var = null);
|
||||
|
||||
/**
|
||||
* Gets the redirect set in the header.
|
||||
*
|
||||
* @param string $var redirect url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function redirect($var = null);
|
||||
|
||||
/**
|
||||
* Returns the clean path to the page file
|
||||
*/
|
||||
public function relativePagePath();
|
||||
|
||||
/**
|
||||
* Gets and sets the path to the folder where the .md for this Page object resides.
|
||||
* This is equivalent to the filePath but without the filename.
|
||||
*
|
||||
* @param string $var the path
|
||||
*
|
||||
* @return string|null the path
|
||||
*/
|
||||
public function path($var = null);
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string $var Optional path
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function folder($var = null);
|
||||
|
||||
/**
|
||||
* Gets and Sets the parent object for this page
|
||||
*
|
||||
* @param PageInterface $var the parent page object
|
||||
*
|
||||
* @return PageInterface|null the parent page object if it exists.
|
||||
*/
|
||||
public function parent(PageInterface $var = null);
|
||||
|
||||
/**
|
||||
* Gets the top parent object for this page
|
||||
*
|
||||
* @return PageInterface|null the top parent page object if it exists.
|
||||
*/
|
||||
public function topParent();
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @return int the index of the current page.
|
||||
*/
|
||||
public function currentPosition();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently configured home page.
|
||||
*
|
||||
* @return bool True if it is the homepage
|
||||
*/
|
||||
public function home();
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the root node of the pages tree.
|
||||
*
|
||||
* @return bool True if it is the root
|
||||
*/
|
||||
public function root();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Interfaces;
|
||||
|
||||
interface PageTranslateInterface
|
||||
{
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Return an array listing untranslated languages available
|
||||
*
|
||||
* @param bool $includeUnpublished also list unpublished translations
|
||||
*
|
||||
* @return array the page untranslated languages
|
||||
*/
|
||||
public function untranslatedLanguages($includeUnpublished = false);
|
||||
|
||||
/**
|
||||
* Get page language
|
||||
*
|
||||
* @param string $var
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function language($var = null);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Markdown;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Medium\Link;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
class Excerpts
|
||||
{
|
||||
/** @var PageInterface */
|
||||
protected $page;
|
||||
/** @var array */
|
||||
protected $config;
|
||||
|
||||
public function __construct(PageInterface $page = null, array $config = null)
|
||||
{
|
||||
$this->page = $page ?? Grav::instance()['page'] ?? null;
|
||||
|
||||
// Add defaults to the configuration.
|
||||
if (null === $config || !isset($config['markdown'], $config['images'])) {
|
||||
$c = Grav::instance()['config'];
|
||||
$config = $config ?? [];
|
||||
$config += [
|
||||
'markdown' => $c->get('system.pages.markdown', []),
|
||||
'images' => $c->get('system.images', [])
|
||||
];
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getPage(): PageInterface
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function fireInitializedEvent($markdown): void
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
$grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Link excerpt
|
||||
*
|
||||
* @param array $excerpt
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function processLinkExcerpt(array $excerpt, string $type = 'link'): array
|
||||
{
|
||||
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
|
||||
|
||||
$url_parts = $this->parseUrl($url);
|
||||
|
||||
// If there is a query, then parse it and build action calls.
|
||||
if (isset($url_parts['query'])) {
|
||||
$actions = array_reduce(
|
||||
explode('&', $url_parts['query']),
|
||||
static function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
|
||||
$carry[$parts[0]] = $value;
|
||||
|
||||
return $carry;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Valid attributes supported.
|
||||
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
|
||||
|
||||
// Unless told to not process, go through actions.
|
||||
if (array_key_exists('noprocess', $actions)) {
|
||||
unset($actions['noprocess']);
|
||||
} else {
|
||||
// Loop through actions for the image and call them.
|
||||
foreach ($actions as $attrib => $value) {
|
||||
$key = $attrib;
|
||||
|
||||
if (in_array($attrib, $valid_attributes, true)) {
|
||||
// support both class and classes.
|
||||
if ($attrib === 'classes') {
|
||||
$attrib = 'class';
|
||||
}
|
||||
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
|
||||
unset($actions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
// If no query elements left, unset query.
|
||||
if (empty($url_parts['query'])) {
|
||||
unset ($url_parts['query']);
|
||||
}
|
||||
|
||||
// Set path to / if not set.
|
||||
if (empty($url_parts['path'])) {
|
||||
$url_parts['path'] = '';
|
||||
}
|
||||
|
||||
// If scheme isn't http(s)..
|
||||
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
|
||||
// Handle custom streams.
|
||||
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
|
||||
$grav = Grav::instance();
|
||||
$url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
|
||||
unset($url_parts['stream'], $url_parts['scheme']);
|
||||
}
|
||||
|
||||
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
// Handle paths and such.
|
||||
$url_parts = Uri::convertUrl($this->page, $url_parts, $type);
|
||||
|
||||
// Build the URL from the component parts and set it on the element.
|
||||
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an image excerpt
|
||||
*
|
||||
* @param array $excerpt
|
||||
* @return array
|
||||
*/
|
||||
public function processImageExcerpt(array $excerpt): array
|
||||
{
|
||||
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
|
||||
$url_parts = $this->parseUrl($url);
|
||||
|
||||
$media = null;
|
||||
$filename = null;
|
||||
|
||||
if (!empty($url_parts['stream'])) {
|
||||
$filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
|
||||
|
||||
$media = $this->page->getMedia();
|
||||
|
||||
} else {
|
||||
$grav = Grav::instance();
|
||||
|
||||
// File is also local if scheme is http(s) and host matches.
|
||||
$local_file = isset($url_parts['path'])
|
||||
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
|
||||
&& (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
|
||||
|
||||
if ($local_file) {
|
||||
$filename = basename($url_parts['path']);
|
||||
$folder = dirname($url_parts['path']);
|
||||
|
||||
// Get the local path to page media if possible.
|
||||
if ($this->page && $folder === $this->page->url(false, false, false)) {
|
||||
// Get the media objects for this page.
|
||||
$media = $this->page->getMedia();
|
||||
} else {
|
||||
// see if this is an external page to this one
|
||||
$base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
|
||||
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
|
||||
|
||||
/** @var PageInterface $ext_page */
|
||||
$ext_page = $grav['pages']->dispatch($page_route, true);
|
||||
if ($ext_page) {
|
||||
$media = $ext_page->getMedia();
|
||||
} else {
|
||||
$grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a media file that matches the path referenced..
|
||||
if ($media && $filename && isset($media[$filename])) {
|
||||
// Get the medium object.
|
||||
/** @var Medium $medium */
|
||||
$medium = $media[$filename];
|
||||
|
||||
// Process operations
|
||||
$medium = $this->processMediaActions($medium, $url_parts);
|
||||
$element_excerpt = $excerpt['element']['attributes'];
|
||||
|
||||
$alt = $element_excerpt['alt'] ?? '';
|
||||
$title = $element_excerpt['title'] ?? '';
|
||||
$class = $element_excerpt['class'] ?? '';
|
||||
$id = $element_excerpt['id'] ?? '';
|
||||
|
||||
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
|
||||
|
||||
} else {
|
||||
// Not a current page media file, see if it needs converting to relative.
|
||||
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process media actions
|
||||
*
|
||||
* @param Medium $medium
|
||||
* @param string|array $url
|
||||
* @return Medium|Link
|
||||
*/
|
||||
public function processMediaActions($medium, $url)
|
||||
{
|
||||
$url_parts = is_string($url) ? $this->parseUrl($url) : $url;
|
||||
$actions = [];
|
||||
|
||||
// if there is a query, then parse it and build action calls
|
||||
if (isset($url_parts['query'])) {
|
||||
$actions = array_reduce(
|
||||
explode('&', $url_parts['query']),
|
||||
static function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = $parts[1] ?? null;
|
||||
$carry[] = ['method' => $parts[0], 'params' => $value];
|
||||
|
||||
return $carry;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
$config = $this->getConfig();
|
||||
if (!empty($config['images']['auto_fix_orientation'])) {
|
||||
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
|
||||
}
|
||||
|
||||
$defaults = $config['images']['defaults'] ?? [];
|
||||
if (count($defaults)) {
|
||||
foreach ($defaults as $method => $params) {
|
||||
$actions[] = [
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// loop through actions for the image and call them
|
||||
foreach ($actions as $action) {
|
||||
$matches = [];
|
||||
|
||||
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
|
||||
$args = [explode(',', $matches[1])];
|
||||
} else {
|
||||
$args = explode(',', $action['params']);
|
||||
}
|
||||
|
||||
$medium = call_user_func_array([$medium, $action['method']], $args);
|
||||
}
|
||||
|
||||
if (isset($url_parts['fragment'])) {
|
||||
$medium->urlHash($url_parts['fragment']);
|
||||
}
|
||||
|
||||
return $medium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of parse_url() which works also with local streams.
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function parseUrl(string $url)
|
||||
{
|
||||
$url_parts = Utils::multibyteParseUrl($url);
|
||||
|
||||
if (isset($url_parts['scheme'])) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
// Special handling for the streams.
|
||||
if ($locator->schemeExists($url_parts['scheme'])) {
|
||||
if (isset($url_parts['host'])) {
|
||||
// Merge host and path into a path.
|
||||
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
|
||||
unset($url_parts['host']);
|
||||
}
|
||||
|
||||
$url_parts['stream'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $url_parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return bool|string
|
||||
*/
|
||||
protected function resolveStream(string $url)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if ($locator->isStream($url)) {
|
||||
return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Yaml;
|
||||
use Grav\Common\Page\Medium\AbstractMedia;
|
||||
use Grav\Common\Page\Medium\GlobalMedia;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
class Media extends AbstractMedia
|
||||
{
|
||||
protected static $global;
|
||||
|
||||
protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param array $media_order
|
||||
* @param bool $load
|
||||
*/
|
||||
public function __construct($path, array $media_order = null, $load = true)
|
||||
{
|
||||
$this->setPath($path);
|
||||
$this->media_order = $media_order;
|
||||
|
||||
$this->__wakeup();
|
||||
if ($load) {
|
||||
$this->init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize static variables on unserialize.
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
if (!isset(static::$global)) {
|
||||
// Add fallback to global media.
|
||||
static::$global = new GlobalMedia();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return parent::offsetExists($offset) ?: isset(static::$global[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return parent::offsetGet($offset) ?: static::$global[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize class.
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$config = Grav::instance()['config'];
|
||||
$locator = Grav::instance()['locator'];
|
||||
$exif_reader = isset(Grav::instance()['exif']) ? Grav::instance()['exif']->getReader() : false;
|
||||
$media_types = array_keys(Grav::instance()['config']->get('media.types'));
|
||||
|
||||
// Handle special cases where page doesn't exist in filesystem.
|
||||
if (!is_dir($this->getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new \FilesystemIterator($this->getPath(), \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
|
||||
|
||||
$media = [];
|
||||
|
||||
/** @var \DirectoryIterator $info */
|
||||
foreach ($iterator as $path => $info) {
|
||||
// Ignore folders and Markdown files.
|
||||
if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find out what type we're dealing with
|
||||
list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename());
|
||||
|
||||
if (!\in_array(strtolower($ext), $media_types, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'alternative') {
|
||||
$media["{$basename}.{$ext}"][$type][$extra] = ['file' => $path, 'size' => $info->getSize()];
|
||||
} else {
|
||||
$media["{$basename}.{$ext}"][$type] = ['file' => $path, 'size' => $info->getSize()];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($media as $name => $types) {
|
||||
// First prepare the alternatives in case there is no base medium
|
||||
if (!empty($types['alternative'])) {
|
||||
foreach ($types['alternative'] as $ratio => &$alt) {
|
||||
$alt['file'] = MediumFactory::fromFile($alt['file']);
|
||||
|
||||
if (!$alt['file']) {
|
||||
unset($types['alternative'][$ratio]);
|
||||
} else {
|
||||
$alt['file']->set('size', $alt['size']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$file_path = null;
|
||||
|
||||
// Create the base medium
|
||||
if (empty($types['base'])) {
|
||||
if (!isset($types['alternative'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$max = max(array_keys($types['alternative']));
|
||||
$medium = $types['alternative'][$max]['file'];
|
||||
$file_path = $medium->path();
|
||||
$medium = MediumFactory::scaledFromMedium($medium, $max, 1)['file'];
|
||||
} else {
|
||||
$medium = MediumFactory::fromFile($types['base']['file']);
|
||||
$medium && $medium->set('size', $types['base']['size']);
|
||||
$file_path = $medium->path();
|
||||
}
|
||||
|
||||
if (empty($medium)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// metadata file
|
||||
$meta_path = $file_path . '.meta.yaml';
|
||||
|
||||
if (file_exists($meta_path)) {
|
||||
$types['meta']['file'] = $meta_path;
|
||||
} elseif ($file_path && $exif_reader && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) {
|
||||
|
||||
$meta = $exif_reader->read($file_path);
|
||||
|
||||
if ($meta) {
|
||||
$meta_data = $meta->getData();
|
||||
$meta_trimmed = array_diff_key($meta_data, array_flip($this->standard_exif));
|
||||
if ($meta_trimmed) {
|
||||
if ($locator->isStream($meta_path)) {
|
||||
$file = File::instance($locator->findResource($meta_path, true, true));
|
||||
} else {
|
||||
$file = File::instance($meta_path);
|
||||
}
|
||||
$file->save(Yaml::dump($meta_trimmed));
|
||||
$types['meta']['file'] = $meta_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($types['meta'])) {
|
||||
$medium->addMetaFile($types['meta']['file']);
|
||||
}
|
||||
|
||||
if (!empty($types['thumb'])) {
|
||||
// We will not turn it into medium yet because user might never request the thumbnail
|
||||
// not wasting any resources on that, maybe we should do this for medium in general?
|
||||
$medium->set('thumbnails.page', $types['thumb']['file']);
|
||||
}
|
||||
|
||||
// Build missing alternatives
|
||||
if (!empty($types['alternative'])) {
|
||||
$alternatives = $types['alternative'];
|
||||
$max = max(array_keys($alternatives));
|
||||
|
||||
for ($i=$max; $i > 1; $i--) {
|
||||
if (isset($alternatives[$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i);
|
||||
}
|
||||
|
||||
foreach ($types['alternative'] as $altMedium) {
|
||||
if ($altMedium['file'] != $medium) {
|
||||
$altWidth = $altMedium['file']->get('width');
|
||||
$medWidth = $medium->get('width');
|
||||
if ($altWidth && $medWidth) {
|
||||
$ratio = $altWidth / $medWidth;
|
||||
$medium->addAlternative($ratio, $altMedium['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->add($name, $medium);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @deprecated 1.6 Use $this->getPath() instead.
|
||||
*/
|
||||
public function path()
|
||||
{
|
||||
return $this->getPath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
||||
use Grav\Common\Media\Interfaces\MediaObjectInterface;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Countable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Iterator;
|
||||
|
||||
abstract class AbstractMedia implements ExportInterface, MediaCollectionInterface
|
||||
{
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
use Export;
|
||||
|
||||
protected $items = [];
|
||||
protected $path;
|
||||
protected $images = [];
|
||||
protected $videos = [];
|
||||
protected $audios = [];
|
||||
protected $files = [];
|
||||
protected $media_order;
|
||||
|
||||
/**
|
||||
* Return media path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function setPath(?string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get medium by filename.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return Medium|null
|
||||
*/
|
||||
public function get($filename)
|
||||
{
|
||||
return $this->offsetGet($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call object as function to get medium by filename.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke($filename)
|
||||
{
|
||||
return $this->offsetGet($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file modification timestamps (query params) for all the media files.
|
||||
*
|
||||
* @param string|int|null $timestamp
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimestamps($timestamp = null)
|
||||
{
|
||||
/** @var Medium $instance */
|
||||
foreach ($this->items as $instance) {
|
||||
$instance->setTimestamp($timestamp);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all media.
|
||||
*
|
||||
* @return MediaObjectInterface[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$this->items = $this->orderMedia($this->items);
|
||||
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all image media.
|
||||
*
|
||||
* @return MediaObjectInterface[]
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
$this->images = $this->orderMedia($this->images);
|
||||
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all video media.
|
||||
*
|
||||
* @return MediaObjectInterface[]
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
$this->videos = $this->orderMedia($this->videos);
|
||||
|
||||
return $this->videos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all audio media.
|
||||
*
|
||||
* @return MediaObjectInterface[]
|
||||
*/
|
||||
public function audios()
|
||||
{
|
||||
$this->audios = $this->orderMedia($this->audios);
|
||||
|
||||
return $this->audios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all file media.
|
||||
*
|
||||
* @return MediaObjectInterface[]
|
||||
*/
|
||||
public function files()
|
||||
{
|
||||
$this->files = $this->orderMedia($this->files);
|
||||
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param MediaObjectInterface $file
|
||||
*/
|
||||
public function add($name, $file)
|
||||
{
|
||||
if (!$file) {
|
||||
return;
|
||||
}
|
||||
$this->offsetSet($name, $file);
|
||||
switch ($file->type) {
|
||||
case 'image':
|
||||
$this->images[$name] = $file;
|
||||
break;
|
||||
case 'video':
|
||||
$this->videos[$name] = $file;
|
||||
break;
|
||||
case 'audio':
|
||||
$this->audios[$name] = $file;
|
||||
break;
|
||||
default:
|
||||
$this->files[$name] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order the media based on the page's media_order
|
||||
*
|
||||
* @param array $media
|
||||
* @return array
|
||||
*/
|
||||
protected function orderMedia($media)
|
||||
{
|
||||
if (null === $this->media_order) {
|
||||
/** @var Page $page */
|
||||
$page = Grav::instance()['pages']->get($this->getPath());
|
||||
|
||||
if ($page && isset($page->header()->media_order)) {
|
||||
$this->media_order = array_map('trim', explode(',', $page->header()->media_order));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->media_order) && is_array($this->media_order)) {
|
||||
$media = Utils::sortArrayByArray($media, $this->media_order);
|
||||
} else {
|
||||
ksort($media, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename, extension and meta part.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return array
|
||||
*/
|
||||
protected function getFileParts($filename)
|
||||
{
|
||||
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
|
||||
$name = $matches[1];
|
||||
$extension = $matches[3];
|
||||
$extra = (int) $matches[2];
|
||||
$type = 'alternative';
|
||||
|
||||
if ($extra === 1) {
|
||||
$type = 'base';
|
||||
$extra = null;
|
||||
}
|
||||
} else {
|
||||
$fileParts = explode('.', $filename);
|
||||
|
||||
$name = array_shift($fileParts);
|
||||
$extension = null;
|
||||
$extra = null;
|
||||
$type = 'base';
|
||||
|
||||
while (($part = array_shift($fileParts)) !== null) {
|
||||
if ($part !== 'meta' && $part !== 'thumb') {
|
||||
if (null !== $extension) {
|
||||
$name .= '.' . $extension;
|
||||
}
|
||||
$extension = $part;
|
||||
} else {
|
||||
$type = $part;
|
||||
$extra = '.' . $part . '.' . implode('.', $fileParts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array($name, $extension, $type, $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
class AudioMedium extends Medium
|
||||
{
|
||||
use StaticResizeTrait;
|
||||
|
||||
/**
|
||||
* Parsedown element for source display mode
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param bool $reset
|
||||
* @return array
|
||||
*/
|
||||
protected function sourceParsedownElement(array $attributes, $reset = true)
|
||||
{
|
||||
$location = $this->url($reset);
|
||||
|
||||
return [
|
||||
'name' => 'audio',
|
||||
'text' => '<source src="' . $location . '">Your browser does not support the audio tag.',
|
||||
'attributes' => $attributes
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set or remove the HTML5 default controls
|
||||
*
|
||||
* @param bool $display
|
||||
* @return $this
|
||||
*/
|
||||
public function controls($display = true)
|
||||
{
|
||||
if($display) {
|
||||
$this->attributes['controls'] = true;
|
||||
} else {
|
||||
unset($this->attributes['controls']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the preload behaviour
|
||||
*
|
||||
* @param string $preload
|
||||
* @return $this
|
||||
*/
|
||||
public function preload($preload)
|
||||
{
|
||||
$validPreloadAttrs = ['auto', 'metadata', 'none'];
|
||||
|
||||
if (\in_array($preload, $validPreloadAttrs, true)) {
|
||||
$this->attributes['preload'] = $preload;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the controlsList behaviour
|
||||
* Separate multiple values with a hyphen
|
||||
*
|
||||
* @param string $controlsList
|
||||
* @return $this
|
||||
*/
|
||||
public function controlsList($controlsList)
|
||||
{
|
||||
$controlsList = str_replace('-', ' ', $controlsList);
|
||||
$this->attributes['controlsList'] = $controlsList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the muted attribute
|
||||
*
|
||||
* @param bool $status
|
||||
* @return $this
|
||||
*/
|
||||
public function muted($status = false)
|
||||
{
|
||||
if($status) {
|
||||
$this->attributes['muted'] = true;
|
||||
} else {
|
||||
unset($this->attributes['muted']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the loop attribute
|
||||
*
|
||||
* @param bool $status
|
||||
* @return $this
|
||||
*/
|
||||
public function loop($status = false)
|
||||
{
|
||||
if($status) {
|
||||
$this->attributes['loop'] = true;
|
||||
} else {
|
||||
unset($this->attributes['loop']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the autoplay attribute
|
||||
*
|
||||
* @param bool $status
|
||||
* @return $this
|
||||
*/
|
||||
public function autoplay($status = false)
|
||||
{
|
||||
if($status) {
|
||||
$this->attributes['autoplay'] = true;
|
||||
} else {
|
||||
unset($this->attributes['autoplay']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset medium.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
parent::reset();
|
||||
|
||||
$this->attributes['controls'] = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
class GlobalMedia extends AbstractMedia
|
||||
{
|
||||
/**
|
||||
* Return media path.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return parent::offsetExists($offset) ?: !empty($this->resolveStream($offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return parent::offsetGet($offset) ?: $this->addMedium($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return string|null
|
||||
*/
|
||||
protected function resolveStream($filename)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
return $locator->isStream($filename) ? ($locator->findResource($filename) ?: null) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stream
|
||||
* @return Medium|null
|
||||
*/
|
||||
protected function addMedium($stream)
|
||||
{
|
||||
$filename = $this->resolveStream($stream);
|
||||
if (!$filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = dirname($filename);
|
||||
list($basename, $ext,, $extra) = $this->getFileParts(basename($filename));
|
||||
$medium = MediumFactory::fromFile($filename);
|
||||
|
||||
if (empty($medium)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$medium->set('size', filesize($filename));
|
||||
$scale = (int) ($extra ?: 1);
|
||||
|
||||
if ($scale !== 1) {
|
||||
$altMedium = $medium;
|
||||
|
||||
// Create scaled down regular sized image.
|
||||
$medium = MediumFactory::scaledFromMedium($altMedium, $scale, 1)['file'];
|
||||
|
||||
if (empty($medium)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add original sized image as alternative.
|
||||
$medium->addAlternative($scale, $altMedium['file']);
|
||||
|
||||
// Locate or generate smaller retina images.
|
||||
for ($i = $scale-1; $i > 1; $i--) {
|
||||
$altFilename = "{$path}/{$basename}@{$i}x.{$ext}";
|
||||
|
||||
if (file_exists($altFilename)) {
|
||||
$scaled = MediumFactory::fromFile($altFilename);
|
||||
} else {
|
||||
$scaled = MediumFactory::scaledFromMedium($altMedium, $scale, $i)['file'];
|
||||
}
|
||||
|
||||
if ($scaled) {
|
||||
$medium->addAlternative($i, $scaled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$meta = "{$path}/{$basename}.{$ext}.yaml";
|
||||
if (file_exists($meta)) {
|
||||
$medium->addMetaFile($meta);
|
||||
}
|
||||
$meta = "{$path}/{$basename}.{$ext}.meta.yaml";
|
||||
if (file_exists($meta)) {
|
||||
$medium->addMetaFile($meta);
|
||||
}
|
||||
|
||||
$thumb = "{$path}/{$basename}.thumb.{$ext}";
|
||||
if (file_exists($thumb)) {
|
||||
$medium->set('thumbnails.page', $thumb);
|
||||
}
|
||||
|
||||
$this->add($stream, $medium);
|
||||
|
||||
return $medium;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Gregwar\Image\Exceptions\GenerationError;
|
||||
use Gregwar\Image\Image;
|
||||
use Gregwar\Image\Source;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
class ImageFile extends Image
|
||||
{
|
||||
public function __destruct()
|
||||
{
|
||||
$this->getAdapter()->deinit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear previously applied operations
|
||||
*/
|
||||
public function clearOperations()
|
||||
{
|
||||
$this->operations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the same as the Gregwar Image class except this one fires a Grav Event on creation of new cached file
|
||||
*
|
||||
* @param string $type the image type
|
||||
* @param int $quality the quality (for JPEG)
|
||||
* @param bool $actual
|
||||
* @param array $extras
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cacheFile($type = 'jpg', $quality = 80, $actual = false, $extras = [])
|
||||
{
|
||||
if ($type === 'guess') {
|
||||
$type = $this->guessType();
|
||||
}
|
||||
|
||||
if (!$this->forceCache && !count($this->operations) && $type === $this->guessType()) {
|
||||
return $this->getFilename($this->getFilePath());
|
||||
}
|
||||
|
||||
// Computes the hash
|
||||
$this->hash = $this->getHash($type, $quality, $extras);
|
||||
|
||||
// Seo friendly image names
|
||||
$seofriendly = Grav::instance()['config']->get('system.images.seofriendly', false);
|
||||
|
||||
if ($seofriendly) {
|
||||
$mini_hash = substr($this->hash, 0, 4) . substr($this->hash, -4);
|
||||
$cacheFile = "{$this->prettyName}-{$mini_hash}";
|
||||
} else {
|
||||
$cacheFile = "{$this->hash}-{$this->prettyName}";
|
||||
}
|
||||
|
||||
$cacheFile .= '.' . $type;
|
||||
|
||||
// If the files does not exists, save it
|
||||
$image = $this;
|
||||
|
||||
// Target file should be younger than all the current image
|
||||
// dependencies
|
||||
$conditions = array(
|
||||
'younger-than' => $this->getDependencies()
|
||||
);
|
||||
|
||||
// The generating function
|
||||
$generate = function ($target) use ($image, $type, $quality) {
|
||||
$result = $image->save($target, $type, $quality);
|
||||
|
||||
if ($result !== $target) {
|
||||
throw new GenerationError($result);
|
||||
}
|
||||
|
||||
Grav::instance()->fireEvent('onImageMediumSaved', new Event(['image' => $target]));
|
||||
};
|
||||
|
||||
// Asking the cache for the cacheFile
|
||||
try {
|
||||
$perms = Grav::instance()['config']->get('system.images.cache_perms', '0755');
|
||||
$perms = octdec($perms);
|
||||
$file = $this->getCacheSystem()->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
|
||||
} catch (GenerationError $e) {
|
||||
$file = $e->getNewFile();
|
||||
}
|
||||
|
||||
// Nulling the resource
|
||||
$this->getAdapter()->setSource(new Source\File($file));
|
||||
$this->getAdapter()->deinit();
|
||||
|
||||
if ($actual) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $this->getFilename($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hash.
|
||||
* @param string $type
|
||||
* @param int $quality
|
||||
* @param [] $extras
|
||||
* @return null
|
||||
*/
|
||||
public function getHash($type = 'guess', $quality = 80, $extras = [])
|
||||
{
|
||||
if (null === $this->hash) {
|
||||
$this->generateHash($type, $quality, $extras);
|
||||
}
|
||||
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the hash.
|
||||
* @param string $type
|
||||
* @param int $quality
|
||||
* @param array $extras
|
||||
*/
|
||||
public function generateHash($type = 'guess', $quality = 80, $extras = [])
|
||||
{
|
||||
$inputInfos = $this->source->getInfos();
|
||||
|
||||
$datas = array(
|
||||
$inputInfos,
|
||||
$this->serializeOperations(),
|
||||
$type,
|
||||
$quality,
|
||||
$extras
|
||||
);
|
||||
|
||||
$this->hash = sha1(serialize($datas));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Page
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
class ImageMedium extends Medium
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $thumbnailTypes = ['page', 'media', 'default'];
|
||||
|
||||
/**
|
||||
* @var ImageFile
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $format = 'guess';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $quality;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $default_quality;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug_watermarked = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $magic_actions = [
|
||||
'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
|
||||
'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
|
||||
'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
|
||||
'rotate', 'flip', 'fixOrientation', 'gaussianBlur'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $magic_resize_actions = [
|
||||
'resize' => [0, 1],
|
||||
'forceResize' => [0, 1],
|
||||
'cropResize' => [0, 1],
|
||||
'crop' => [0, 1, 2, 3],
|
||||
'zoomCrop' => [0, 1]
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $sizes = '100vw';
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param array $items
|
||||
* @param Blueprint $blueprint
|
||||
*/
|
||||
public function __construct($items = [], Blueprint $blueprint = null)
|
||||
{
|
||||
parent::__construct($items, $blueprint);
|
||||
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
$path = $this->get('filepath');
|
||||
if (!$path || !file_exists($path) || !filesize($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image_info = getimagesize($path);
|
||||
|
||||
$this->def('width', $image_info[0]);
|
||||
$this->def('height', $image_info[1]);
|
||||
$this->def('mime', $image_info['mime']);
|
||||
$this->def('debug', $config->get('system.images.debug'));
|
||||
|
||||
$this->set('thumbnails.media', $this->get('filepath'));
|
||||
|
||||
$this->default_quality = $config->get('system.images.default_image_quality', 85);
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($config->get('system.images.cache_all', false)) {
|
||||
$this->cache();
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
unset($this->image);
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->image = $this->image ? clone $this->image : null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta file for the medium.
|
||||
*
|
||||
* @param string $filepath
|
||||
* @return $this
|
||||
*/
|
||||
public function addMetaFile($filepath)
|
||||
{
|
||||
parent::addMetaFile($filepath);
|
||||
|
||||
// Apply filters in meta file
|
||||
$this->reset();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear out the alternatives
|
||||
*/
|
||||
public function clearAlternatives()
|
||||
{
|
||||
$this->alternatives = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return PATH to image.
|
||||
*
|
||||
* @param bool $reset
|
||||
* @return string path to image
|
||||
*/
|
||||
public function path($reset = true)
|
||||
{
|
||||
$output = $this->saveImage();
|
||||
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URL to image.
|
||||
*
|
||||
* @param bool $reset
|
||||
* @return string
|
||||
*/
|
||||
public function url($reset = true)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
|
||||
$saved_image_path = $this->saveImage();
|
||||
|
||||
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
|
||||
|
||||
if ($locator->isStream($output)) {
|
||||
$output = $locator->findResource($output, false);
|
||||
}
|
||||
|
||||
if (Utils::startsWith($output, $image_path)) {
|
||||
$image_dir = $locator->findResource('cache://images', false);
|
||||
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
|
||||
}
|
||||
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply processes with no extra methods. Useful for triggering events.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function cache()
|
||||
{
|
||||
if (!$this->image) {
|
||||
$this->image();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return srcset string for this Medium and its alternatives.
|
||||
*
|
||||
* @param bool $reset
|
||||
* @return string
|
||||
*/
|
||||
public function srcset($reset = true)
|
||||
{
|
||||
if (empty($this->alternatives)) {
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$srcset = [];
|
||||
foreach ($this->alternatives as $ratio => $medium) {
|
||||
$srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
|
||||
}
|
||||
$srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
|
||||
|
||||
return implode(', ', $srcset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the ability to override the image's pretty name stored in cache
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function setImagePrettyName($name)
|
||||
{
|
||||
$this->set('prettyname', $name);
|
||||
if ($this->image) {
|
||||
$this->image->setPrettyName($name);
|
||||
}
|
||||
}
|
||||
|
||||
public function getImagePrettyName()
|
||||
{
|
||||
if ($this->get('prettyname')) {
|
||||
return $this->get('prettyname');
|
||||
}
|
||||
|
||||
$basename = $this->get('basename');
|
||||
if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
|
||||
$basename = $matches[1];
|
||||
}
|
||||
return $basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate alternative image widths, using either an array of integers, or
|
||||
* a min width, a max width, and a step parameter to fill out the necessary
|
||||
* widths. Existing image alternatives won't be overwritten.
|
||||
*
|
||||
* @param int|int[] $min_width
|
||||
* @param int $max_width
|
||||
* @param int $step
|
||||
* @return $this
|
||||
*/
|
||||
public function derivatives($min_width, $max_width = 2500, $step = 200)
|
||||
{
|
||||
if (!empty($this->alternatives)) {
|
||||
$max = max(array_keys($this->alternatives));
|
||||
$base = $this->alternatives[$max];
|
||||
} else {
|
||||
$base = $this;
|
||||
}
|
||||
|
||||
$widths = [];
|
||||
|
||||
if (func_num_args() === 1) {
|
||||
foreach ((array) func_get_arg(0) as $width) {
|
||||
if ($width < $base->get('width')) {
|
||||
$widths[] = $width;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$max_width = min($max_width, $base->get('width'));
|
||||
|
||||
for ($width = $min_width; $width < $max_width; $width = $width + $step) {
|
||||
$widths[] = $width;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($widths as $width) {
|
||||
// Only generate image alternatives that don't already exist
|
||||
if (array_key_exists((int) $width, $this->alternatives)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$derivative = MediumFactory::fromFile($base->get('filepath'));
|
||||
|
||||
// It's possible that MediumFactory::fromFile returns null if the
|
||||
// original image file no longer exists and this class instance was
|
||||
// retrieved from the page cache
|
||||
if (null !== $derivative) {
|
||||
$index = 2;
|
||||
$alt_widths = array_keys($this->alternatives);
|
||||
sort($alt_widths);
|
||||
|
||||
foreach ($alt_widths as $i => $key) {
|
||||
if ($width > $key) {
|
||||
$index += max($i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$basename = preg_replace('/(@\d+x){0,1}$/', "@{$width}w", $base->get('basename'), 1);
|
||||
$derivative->setImagePrettyName($basename);
|
||||
|
||||
$ratio = $base->get('width') / $width;
|
||||
$height = $derivative->get('height') / $ratio;
|
||||
|
||||
$derivative->resize($width, $height);
|
||||
$derivative->set('width', $width);
|
||||
$derivative->set('height', $height);
|
||||
|
||||
$this->addAlternative($ratio, $derivative);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsedown element for source display mode
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param bool $reset
|
||||
* @return array
|
||||
*/
|
||||
public function sourceParsedownElement(array $attributes, $reset = true)
|
||||
{
|
||||
empty($attributes['src']) && $attributes['src'] = $this->url(false);
|
||||
|
||||
$srcset = $this->srcset($reset);
|
||||
if ($srcset) {
|
||||
empty($attributes['srcset']) && $attributes['srcset'] = $srcset;
|
||||
$attributes['sizes'] = $this->sizes();
|
||||
}
|
||||
|
||||
return ['name' => 'img', 'attributes' => $attributes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset image.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
parent::reset();
|
||||
|
||||
if ($this->image) {
|
||||
$this->image();
|
||||
$this->medium_querystring = [];
|
||||
$this->filter();
|
||||
$this->clearAlternatives();
|
||||
}
|
||||
|
||||
$this->format = 'guess';
|
||||
$this->quality = $this->default_quality;
|
||||
|
||||
$this->debug_watermarked = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn the current Medium into a Link
|
||||
*
|
||||
* @param bool $reset
|
||||
* @param array $attributes
|
||||
* @return Link
|
||||
*/
|
||||
public function link($reset = true, array $attributes = [])
|
||||
{
|
||||
$attributes['href'] = $this->url(false);
|
||||
$srcset = $this->srcset(false);
|
||||
if ($srcset) {
|
||||
$attributes['data-srcset'] = $srcset;
|
||||
}
|
||||
|
||||
return parent::link($reset, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn the current Medium into a Link with lightbox enabled
|
||||
*
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $reset
|
||||
* @return Link
|
||||
*/
|
||||
public function lightbox($width = null, $height = null, $reset = true)
|
||||
{
|
||||
if ($this->mode !== 'source') {
|
||||
$this->display('source');
|
||||
}
|
||||
|
||||
if ($width && $height) {
|
||||
$this->__call('cropResize', [$width, $height]);
|
||||
}
|
||||
|
||||
return parent::lightbox($width, $height, $reset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or gets the quality of the image
|
||||
*
|
||||
* @param int $quality 0-100 quality
|
||||
* @return int|$this
|
||||
*/
|
||||
public function quality($quality = null)
|
||||
{
|
||||
if ($quality) {
|
||||
if (!$this->image) {
|
||||
$this->image();
|
||||
}
|
||||
|
||||
$this->quality = $quality;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets image output format.
|
||||
*
|
||||
* @param string $format
|
||||
* @return $this
|
||||
*/
|
||||
public function format($format)
|
||||
{
|
||||
if (!$this->image) {
|
||||
$this->image();
|
||||
}
|
||||
|
||||
$this->format = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or get sizes parameter for srcset media action
|
||||
*
|
||||
* @param string $sizes
|
||||
* @return string
|
||||
*/
|
||||
public function sizes($sizes = null)
|
||||
{
|
||||
|
||||
if ($sizes) {
|
||||
$this->sizes = $sizes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return empty($this->sizes) ? '100vw' : $this->sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the width attribute from Markdown or Twig
|
||||
* Examples: 
|
||||
* 
|
||||
* 
|
||||
* 
|
||||
* {{ page.media['myimg.png'].width().height().html }}
|
||||
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
||||
*
|
||||
* @param mixed $value A value or 'auto' or empty to use the width of the image
|
||||
* @return $this
|
||||
*/
|
||||
public function width($value = 'auto')
|
||||
{
|
||||
if (!$value || $value === 'auto') {
|
||||
$this->attributes['width'] = $this->get('width');
|
||||
} else {
|
||||
$this->attributes['width'] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the height attribute from Markdown or Twig
|
||||
* Examples: 
|
||||
* 
|
||||
* 
|
||||
* 
|
||||
* {{ page.media['myimg.png'].width().height().html }}
|
||||
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
||||
*
|
||||
* @param mixed $value A value or 'auto' or empty to use the height of the image
|
||||
* @return $this
|
||||
*/
|
||||
public function height($value = 'auto')
|
||||
{
|
||||
if (!$value || $value === 'auto') {
|
||||
$this->attributes['height'] = $this->get('height');
|
||||
} else {
|
||||
$this->attributes['height'] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the call to the image processing method.
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $args
|
||||
* @return $this|mixed
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if ($method === 'cropZoom') {
|
||||
$method = 'zoomCrop';
|
||||
}
|
||||
|
||||
if (!\in_array($method, self::$magic_actions, true)) {
|
||||
return parent::__call($method, $args);
|
||||
}
|
||||
|
||||
// Always initialize image.
|
||||
if (!$this->image) {
|
||||
$this->image();
|
||||
}
|
||||
|
||||
try {
|
||||
call_user_func_array([$this->image, $method], $args);
|
||||
|
||||
foreach ($this->alternatives as $medium) {
|
||||
if (!$medium->image) {
|
||||
$medium->image();
|
||||
}
|
||||
|
||||
$args_copy = $args;
|
||||
|
||||
// regular image: resize 400x400 -> 200x200
|
||||
// --> @2x: resize 800x800->400x400
|
||||
if (isset(self::$magic_resize_actions[$method])) {
|
||||
foreach (self::$magic_resize_actions[$method] as $param) {
|
||||
if (isset($args_copy[$param])) {
|
||||
$args_copy[$param] *= $medium->get('ratio');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
call_user_func_array([$medium, $method], $args_copy);
|
||||
}
|
||||
} catch (\BadFunctionCallException $e) {
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets medium image, resets image manipulation operations.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function image()
|
||||
{
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$file = $this->get('filepath');
|
||||
|
||||
// Use existing cache folder or if it doesn't exist, create it.
|
||||
$cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
|
||||
|
||||
// Make sure we free previous image.
|
||||
unset($this->image);
|
||||
|
||||
$this->image = ImageFile::open($file)
|
||||
->setCacheDir($cacheDir)
|
||||
->setActualCacheDir($cacheDir)
|
||||
->setPrettyName($this->getImagePrettyName());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the image with cache.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function saveImage()
|
||||
{
|
||||
if (!$this->image) {
|
||||
return parent::path(false);
|
||||
}
|
||||
|
||||
$this->filter();
|
||||
|
||||
if (isset($this->result)) {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
if (!$this->debug_watermarked && $this->get('debug')) {
|
||||
$ratio = $this->get('ratio');
|
||||
if (!$ratio) {
|
||||
$ratio = 1;
|
||||
}
|
||||
|
||||
$locator = Grav::instance()['locator'];
|
||||
$overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
|
||||
$this->image->merge(ImageFile::open($overlay));
|
||||
}
|
||||
|
||||
return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter image by using user defined filter parameters.
|
||||
*
|
||||
* @param string $filter Filter to be used.
|
||||
*/
|
||||
public function filter($filter = 'image.filters.default')
|
||||
{
|
||||
$filters = (array) $this->get($filter, []);
|
||||
foreach ($filters as $params) {
|
||||
$params = (array) $params;
|
||||
$method = array_shift($params);
|
||||
$this->__call($method, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the image higher quality version
|
||||
*
|
||||
* @return ImageMedium the alternative version with higher quality
|
||||
*/
|
||||
public function higherQualityAlternative()
|
||||
{
|
||||
if ($this->alternatives) {
|
||||
$max = reset($this->alternatives);
|
||||
foreach($this->alternatives as $alternative)
|
||||
{
|
||||
if($alternative->quality() > $max->quality())
|
||||
{
|
||||
$max = $alternative;
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user