first commit

This commit is contained in:
2019-03-28 17:57:56 +01:00
commit b0e25fd66f
561 changed files with 56803 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
<?php
/**
* @package Grav.Common.Backup
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Backup;
use Grav\Common\Grav;
use Grav\Common\Inflector;
class ZipBackup
{
protected static $ignorePaths = [
'backup',
'cache',
'images',
'logs',
'tmp'
];
protected static $ignoreFolders = [
'.git',
'.svn',
'.hg',
'.idea',
'node_modules'
];
/**
* Backup
*
* @param string|null $destination
* @param callable|null $messager
*
* @return null|string
*/
public static function backup($destination = null, callable $messager = null)
{
if (!$destination) {
$destination = Grav::instance()['locator']->findResource('backup://', true);
if (!$destination) {
throw new \RuntimeException('The backup folder is missing.');
}
}
$name = substr(strip_tags(Grav::instance()['config']->get('site.title', basename(GRAV_ROOT))), 0, 20);
$inflector = new Inflector();
if (is_dir($destination)) {
$date = date('YmdHis', time());
$filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip';
$destination = rtrim($destination, DS) . DS . $filename;
}
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => 'Creating new Backup "' . $destination . '"'
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => ''
]);
$zip = new \ZipArchive();
$zip->open($destination, \ZipArchive::CREATE);
$max_execution_time = ini_set('max_execution_time', 600);
static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
$messager && $messager([
'type' => 'progress',
'percentage' => false,
'complete' => true
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => ''
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => 'Saving and compressing archive...'
]);
$zip->close();
if ($max_execution_time !== false) {
ini_set('max_execution_time', $max_execution_time);
}
return $destination;
}
/**
* @param $folder
* @param $zipFile
* @param $exclusiveLength
* @param $messager
*/
private static function folderToZip($folder, \ZipArchive $zipFile, $exclusiveLength, callable $messager = null)
{
$handle = opendir($folder);
while (false !== $f = readdir($handle)) {
if ($f !== '.' && $f !== '..') {
$filePath = "$folder/$f";
// Remove prefix from file path before add to zip.
$localPath = substr($filePath, $exclusiveLength);
if (in_array($f, static::$ignoreFolders)) {
continue;
}
if (in_array($localPath, static::$ignorePaths)) {
$zipFile->addEmptyDir($f);
continue;
}
if (is_file($filePath)) {
$zipFile->addFile($filePath, $localPath);
$messager && $messager([
'type' => 'progress',
'percentage' => false,
'complete' => false
]);
} elseif (is_dir($filePath)) {
// Add sub-directory.
$zipFile->addEmptyDir($localPath);
static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
}
}
}
closedir($handle);
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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 intval($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;
}
}

View File

@@ -0,0 +1,510 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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 RocketTheme\Toolbox\Event\Event;
/**
* The GravCache object is used throughout Grav to store and retrieve cached data.
* It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
*
* APCu
* APC
* XCache
* RedisCache
* MemCache
* MemCacheD
* 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;
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();
$this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
/** @var Uri $uri */
$uri = $grav['uri'];
$prefix = $this->config->get('system.cache.prefix');
if (is_null($this->enabled)) {
$this->enabled = (bool)$this->config->get('system.cache.enabled');
}
// Cache key allows us to invalidate all cache on configuration changes.
$this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
2, 8);
$this->driver_setting = $this->config->get('system.cache.driver');
$this->driver = $this->getCacheDriver();
// Set the cache namespace to our unique key
$this->driver->setNamespace($this->key);
}
/**
* Public accessor to set the enabled state of the cache
*
* @param $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('apc')) {
$driver_name = 'apc';
} elseif (extension_loaded('wincache')) {
$driver_name = 'wincache';
} elseif (extension_loaded('xcache')) {
$driver_name = 'xcache';
}
} else {
$driver_name = $setting;
}
$this->driver_name = $driver_name;
switch ($driver_name) {
case 'apc':
$driver = new DoctrineCache\ApcCache();
break;
case 'apcu':
$driver = new DoctrineCache\ApcuCache();
break;
case 'wincache':
$driver = new DoctrineCache\WinCacheCache();
break;
case 'xcache':
$driver = new DoctrineCache\XcacheCache();
break;
case 'memcache':
$memcache = new \Memcache();
$memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
$this->config->get('system.cache.memcache.port', 11211));
$driver = new DoctrineCache\MemcacheCache();
$driver->setMemcache($memcache);
break;
case 'memcached':
$memcached = new \Memcached();
$memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
$this->config->get('system.cache.memcached.port', 11211));
$driver = new DoctrineCache\MemcachedCache();
$driver->setMemcached($memcached);
break;
case 'redis':
$redis = new \Redis();
$socket = $this->config->get('system.cache.redis.socket', false);
$password = $this->config->get('system.cache.redis.password', false);
if ($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);
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);
} else {
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;
}
/**
* 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;
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;
}
}
// 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;
}
/**
* Set the cache lifetime programmatically
*
* @param int $future timestamp
*/
public function setLifetime($future)
{
if (!$future) {
return;
}
$interval = $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 = $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 $setting
* @return bool
*/
public function isVolatileDriver($setting)
{
if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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') || strtolower(substr(PHP_OS, 0, 3)) === 'win') {
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, 'r');
$firstLine = fgets($file);
fclose($file);
if (!preg_match('/^#!.+php/i', $firstLine)) {
$executor = '';
}
}
return $executor . $composer;
}
}

View File

@@ -0,0 +1,264 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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 (!isset($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'])
|| !isset($cache['data'])
|| !isset($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 = isset($cache['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();
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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 extends CompiledBase
{
/**
* @var int Version number for the compiled file.
*/
public $version = 2;
/**
* @var BlueprintSchema Blueprints object.
*/
protected $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) . 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();
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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 int Version number for the compiled file.
*/
public $version = 1;
/**
* @var Config Configuration object.
*/
protected $object;
/**
* @var callable Blueprints loader.
*/
protected $callable;
/**
* @var bool
*/
protected $withDefaults;
/**
* 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();
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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
{
/**
* @var int Version number for the compiled file.
*/
public $version = 1;
/**
* @var Languages Configuration object.
*/
protected $object;
/**
* 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();
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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
{
/** @var string */
protected $checksum;
protected $modified = false;
protected $timestamp = 0;
public function key()
{
return $this->checksum();
}
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);
}
}
// Override the media.upload_limit based on PHP values
$upload_limit = Utils::getUploadLimit();
$this->items['system']['media']['upload_limit'] = $upload_limit > 0 ? $upload_limit : 1024*1024*1024;
}
/**
* @return mixed
* @deprecated
*/
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'];
}
}

View File

@@ -0,0 +1,262 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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
{
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);
}
}

View File

@@ -0,0 +1,283 @@
<?php
/**
* @package Grav.Common.Config
*
* @copyright Copyright (C) 2015 - 2018 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 RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Setup extends Data
{
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' => 'ReadOnlyStream',
'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' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://images', 'system://images']
]
],
'page' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://pages']
]
],
'account' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
'' => ['user://accounts']
]
],
];
/**
* @param Container|array $container
*/
public function __construct($container)
{
$environment = null !== static::$environment ? static::$environment : ($container['uri']->environment() ?: 'localhost');
// Pre-load setup.php which contains our initial configuration.
// Configuration may contain dynamic parts, which is why we need to always load it.
// If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults.
$file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php';
$setup = is_file($file) ? (array) include $file : [];
// 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 ?: 'cli');
$this->def('streams.schemes.environment.prefixes', ['' => $environment ? ["user://{$this->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 = isset($config['override']) ? $config['override'] : false;
$force = isset($config['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 = !empty($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 = isset($this->items['streams']['schemes']) ? $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);
$file = YamlFile::instance($filename);
if (!$file->exists()) {
$file->save(['salt' => Utils::generateRandomString(14)]);
$file->free();
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
}
}
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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 RocketTheme\Toolbox\Blueprints\BlueprintForm;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Blueprint extends BlueprintForm
{
/**
* @var string
*/
protected $context = 'blueprints://';
/**
* @var BlueprintSchema
*/
protected $blueprintSchema;
/**
* 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();
return $this->blueprintSchema->getDefaults();
}
/**
* 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);
}
/**
* 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
* @return array
*/
public function filter(array $data)
{
$this->initInternals();
return $this->blueprintSchema->filter($data);
}
/**
* Return blueprint data schema.
*
* @return BlueprintSchema
*/
public function schema()
{
$this->initInternals();
return $this->blueprintSchema;
}
/**
* Initialize validator.
*/
protected function initInternals()
{
if (!isset($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();
}
}
/**
* @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 = isset($this->overrides[$path]) ? (array) $this->overrides[$path] : [];
// 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 = [];
}
list($o, $f) = preg_split('/::/', $function, 2);
if (!$f) {
if (function_exists($o)) {
$data = call_user_func_array($o, $params);
}
} else {
if (method_exists($o, $f)) {
$data = call_user_func_array(array($o, $f), $params);
}
}
// If function returns a value,
if (isset($data)) {
if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
// 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 = isset($field[$property]) ? $field[$property] : null;
$config = Grav::instance()['config']->get($value, $default);
if (!is_null($config)) {
$field[$property] = $config;
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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
];
/**
* 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);
}
}
/**
* Filter data by using blueprints.
*
* @param array $data
* @return array
*/
public function filter(array $data)
{
return $this->filterArray($data, $this->nested);
}
/**
* @param array $data
* @param array $rules
* @returns array
* @throws \RuntimeException
* @internal
*/
protected function validateArray(array $data, array $rules)
{
$messages = $this->checkRequired($data, $rules);
foreach ($data as $key => $field) {
$val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
$rule = is_string($val) ? $this->items[$val] : null;
if ($rule) {
// Item has been defined in blueprints.
$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
* @return array
* @internal
*/
protected function filterArray(array $data, array $rules)
{
$results = array();
foreach ($data as $key => $field) {
$val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
$rule = is_string($val) ? $this->items[$val] : null;
if ($rule) {
// Item has been defined in blueprints.
$field = Validation::filter($field, $rule);
} elseif (is_array($field) && is_array($val)) {
// Array has been defined in blueprints.
$field = $this->filterArray($field, $val);
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
$field = null;
}
if (isset($field) && (!is_array($field) || !empty($field))) {
$results[$key] = $field;
}
}
return $results;
}
/**
* @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];
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 = isset($field['label']) ? $field['label'] : $field['name'];
$language = Grav::instance()['language'];
$message = sprintf($language->translate('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 = isset($field[$property]) ? $field[$property] : null;
$config = Grav::instance()['config']->get($value, $default);
if (null !== $config) {
$field[$property] = $config;
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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
{
protected $search;
protected $types;
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])) {
$this->instances[$type] = $this->loadFile($type);
}
return $this->instances[$type];
}
/**
* Get all available blueprint types.
*
* @return array List of type=>name
*/
public function types()
{
if ($this->types === null) {
$this->types = array();
$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);
}
return $blueprint->load()->init();
}
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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, ExportInterface
{
use NestedArrayAccessWithGetters, Countable, Export;
protected $gettersVariable = 'items';
protected $items;
/**
* @var Blueprints
*/
protected $blueprints;
/**
* @var File
*/
protected $storage;
/**
* @param array $items
* @param Blueprint|callable $blueprints
*/
public function __construct(array $items = array(), $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 $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
* Filter all items by using blueprints.
*/
public function filter()
{
$this->items = $this->blueprints()->filter($this->items);
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;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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);
}

View File

@@ -0,0 +1,765 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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;
use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYaml;
class Validation
{
/**
* Validate value against a blueprint field definition.
*
* @param $value
* @param array $field
* @return array
*/
public static function validate($value, array $field)
{
$messages = [];
$validate = isset($field['validate']) ? (array) $field['validate'] : [];
// Validate type with fallback type text.
$type = (string) isset($validate['type']) ? $validate['type'] : $field['type'];
$method = 'type'.strtr($type, '-', '_');
// If value isn't required, we will stop validation if empty value is given.
if ((empty($validate['required']) || (isset($validate['required']) && $validate['required'] !== true)) && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))) {
return $messages;
}
if (!isset($field['type'])) {
$field['type'] = 'text';
}
// Get language class.
$language = Grav::instance()['language'];
$name = ucfirst(isset($field['label']) ? $field['label'] : $field['name']);
$message = (string) isset($field['validate']['message'])
? $language->translate($field['validate']['message'])
: $language->translate('FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
// If this is a YAML field validate/filter as such
if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
$method = 'typeYaml';
}
if (method_exists(__CLASS__, $method)) {
$success = self::$method($value, $validate, $field);
} else {
$success = true;
}
if (!$success) {
$messages[$field['name']][] = $message;
}
// Check individual rules.
foreach ($validate as $rule => $params) {
$method = 'validate' . ucfirst(strtr($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 = isset($field['validate']) ? (array) $field['validate'] : [];
// If value isn't required, we will return null if empty value is given.
if (empty($validate['required']) && ($value === null || $value === '')) {
return null;
}
if (!isset($field['type'])) {
$field['type'] = 'text';
}
// Validate type with fallback type text.
$type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
$method = 'filter' . ucfirst(strtr($type, '-', '_'));
// If this is a YAML field validate/filter as such
if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
$method = 'filterYaml';
}
if (!method_exists(__CLASS__, $method)) {
$method = '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 (isset($params['min']) && strlen($value) < $params['min']) {
return false;
}
if (isset($params['max']) && strlen($value) > $params['max']) {
return false;
}
$min = isset($params['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)
{
return (string) $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;
if (!isset($field['value'])) {
$field['value'] = 1;
}
if (isset($value) && $value != $field['value']) {
return false;
}
return true;
}
/**
* 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)
{
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 = isset($params['min']) ? $params['min'] : 0;
if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) {
return false;
}
return true;
}
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 $value) {
if (!(self::typeText($value, $params, $field) && filter_var($value, 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;
} elseif (!is_string($value)) {
return false;
} elseif (!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)
{
$params = array($params);
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)
{
$params = array($params);
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)
{
$params = array($params);
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 = isset($params['min']) ? $params['min'] : 0;
if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) {
return false;
}
}
$options = isset($field['options']) ? array_keys($field['options']) : array();
$values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value;
if ($options && array_diff($values, $options)) {
return false;
}
return true;
}
protected static function filterArray($value, $params, $field)
{
$values = (array) $value;
$options = isset($field['options']) ? array_keys($field['options']) : array();
$multi = isset($field['multiple']) ? $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 => $value) {
$values[$key] = $useKey ? (bool) $value : $value;
}
}
if ($multi) {
foreach ($values as $key => $value) {
if (is_array($value)) {
$value = implode(',', $value);
$values[$key] = array_map('trim', explode(',', $value));
} else {
$values[$key] = trim($value);
}
}
}
if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) {
foreach ($values as $key => $value) {
foreach ($value as $inner_key => $inner_value) {
if ($inner_value == '') {
unset($value[$inner_key]);
}
}
$values[$key] = $value;
}
}
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 = isset($item[$subKey]) ? $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;
}
// 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 !== '';
} else {
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));
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* @package Grav.Common.Data
*
* @copyright Copyright (C) 2015 - 2018 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('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;
}
}

View File

@@ -0,0 +1,443 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use DebugBar\DataCollector\ConfigCollector;
use DebugBar\DataCollector\MessagesCollector;
use DebugBar\JavascriptRenderer;
use DebugBar\StandardDebugBar;
use Grav\Common\Config\Config;
class Debugger
{
/** @var Grav $grav */
protected $grav;
/** @var Config $config */
protected $config;
/** @var JavascriptRenderer $renderer */
protected $renderer;
/** @var StandardDebugBar $debugbar */
protected $debugbar;
protected $enabled;
protected $timers = [];
/** @var string[] $deprecations */
protected $deprecations = [];
protected $errorHandler;
/**
* Debugger constructor.
*/
public function __construct()
{
// Enable debugger until $this->init() gets called.
$this->enabled = true;
$this->debugbar = new StandardDebugBar();
$this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
// Set deprecation collector.
$this->setErrorHandler();
}
/**
* Initialize the debugger
*
* @return $this
* @throws \DebugBar\DebugBarException
*/
public function init()
{
$this->grav = Grav::instance();
$this->config = $this->grav['config'];
// Enable/disable debugger based on configuration.
$this->enabled = $this->config->get('system.debugger.enabled');
if ($this->enabled()) {
$plugins_config = (array)$this->config->get('plugins');
ksort($plugins_config);
$this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
$this->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 null
*/
public function enabled($state = null)
{
if ($state !== null) {
$this->enabled = $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 $collector
*
* @return $this
* @throws \DebugBar\DebugBarException
*/
public function addCollector($collector)
{
$this->debugbar->addCollector($collector);
return $this;
}
/**
* Returns a data collector
*
* @param $collector
*
* @return \DebugBar\DataCollector\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 $name
* @param string|null $description
*
* @return $this
*/
public function startTimer($name, $description = null)
{
if ($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) && ($name[0] === '_' || $this->enabled())) {
$this->debugbar['time']->stopMeasure($name);
}
return $this;
}
/**
* Dump variables into the Messages tab of the Debug Bar
*
* @param $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->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;
}
$backtrace = debug_backtrace(false);
// Skip current call.
array_shift($backtrace);
// Skip vendor libraries and the method where error was triggered.
while ($current = array_shift($backtrace)) {
if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) {
continue;
}
if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
$current = array_shift($backtrace);
}
break;
}
// Add back last call.
array_unshift($backtrace, $current);
// Filter arguments.
foreach ($backtrace as &$current) {
if (isset($current['args'])) {
$args = [];
foreach ($current['args'] as $arg) {
if (\is_string($arg)) {
$args[] = "'" . $arg . "'";
} elseif (\is_bool($arg)) {
$args[] = $arg ? 'true' : 'false';
} elseif (\is_scalar($arg)) {
$args[] = $arg;
} elseif (\is_object($arg)) {
$args[] = get_class($arg) . ' $object';
} elseif (\is_array($arg)) {
$args[] = '$array';
} else {
$args[] = '$object';
}
}
$current['args'] = $args;
}
}
unset($current);
$this->deprecations[] = [
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'trace' => $backtrace,
];
// 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 = 'unknown';
if (stripos($deprecated['message'], 'grav') !== false) {
$scope = 'grav';
} elseif (!isset($deprecated['file'])) {
$scope = 'unknown';
} elseif (stripos($deprecated['file'], 'twig') !== false) {
$scope = 'twig';
} elseif (stripos($deprecated['file'], 'yaml') !== false) {
$scope = 'yaml';
} elseif (stripos($deprecated['file'], 'vendor') !== false) {
$scope = 'vendor';
}
$trace = [];
foreach ($deprecated['trace'] as $current) {
$class = isset($current['class']) ? $current['class'] : '';
$type = isset($current['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']);
$trace[] = ['call' => $class . $type . $function] + $current;
}
return [
[
'message' => $deprecated['message'],
'trace' => $trace
],
$scope
];
}
protected function getFunction($trace)
{
if (!isset($trace['function'])) {
return '';
}
return $trace['function'] . '(' . implode(', ', $trace['args']) . ')';
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* @package Grav.Common.Errors
*
* @copyright Copyright (C) 2015 - 2018 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;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @package Grav.Common.Errors
*
* @copyright Copyright (C) 2015 - 2018 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 (method_exists('Whoops\Util\Misc', 'isAjaxRequest')) { //Whoops 2.0
if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
$whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
}
} elseif (function_exists('Whoops\isAjaxRequest')) { //Whoops 2.0.0-alpha
if (Whoops\isAjaxRequest() || $jsonRequest) {
$whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
}
} else { //Whoops 1.x
$json_page = new Whoops\Handler\JsonResponseHandler;
$json_page->onlyForAjaxRequests(true);
}
if (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;
}
}, 'log');
}
$whoops->register();
// Re-register deprecation handler.
$grav['debugger']->setErrorHandler();
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -0,0 +1,107 @@
<?php
/**
* @package Grav.Common.Errors
*
* @copyright Copyright (C) 2015 - 2018 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 $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;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @package Grav.Common.Errors
*
* @copyright Copyright (C) 2015 - 2018 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();
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* @package Grav.Common.File
*
* @copyright Copyright (C) 2015 - 2018 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;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav.Common.File
*
* @copyright Copyright (C) 2015 - 2018 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);
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @package Grav.Common.File
*
* @copyright Copyright (C) 2015 - 2018 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;
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @package Grav.Common.File
*
* @copyright Copyright (C) 2015 - 2018 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;
}

View File

@@ -0,0 +1,491 @@
<?php
/**
* @package Grav.Common.FileSystem
*
* @copyright Copyright (C) 2015 - 2018 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 $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)
{
$base = preg_replace('![\\\/]+!', '/', $base);
$path = preg_replace('![\\\/]+!', '/', $path);
if ($path === $base) {
return '';
}
$baseParts = explode('/', isset($base[0]) && '/' === $base[0] ? substr($base, 1) : $base);
$pathParts = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $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
|| '/' === $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 = isset($params['pattern']) ? $params['pattern'] : null;
$filters = isset($params['filters']) ? $params['filters'] : null;
$recursive = isset($params['recursive']) ? $params['recursive'] : true;
$levels = isset($params['levels']) ? $params['levels'] : -1;
$key = isset($params['key']) ? 'get' . $params['key'] : null;
$value = isset($params['value']) ? 'get' . $params['value'] : ($recursive ? 'getSubPathname' : 'getFilename');
$folders = isset($params['folders']) ? $params['folders'] : true;
$files = isset($params['files']) ? $params['files'] : true;
/** @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 ($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 = call_user_func($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']);
}
// 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)
{
if (is_dir($folder)) {
return;
}
$success = @mkdir($folder, 0777, true);
if (!$success) {
$error = error_get_last();
throw new \RuntimeException($error['message']);
}
}
/**
* Recursive copy of one directory to another
*
* @param $src
* @param $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::mkdir($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 (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;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package Grav.Common.FileSystem
*
* @copyright Copyright (C) 2015 - 2018 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 $folder_ignores;
/**
* Create a RecursiveFilterIterator from a RecursiveIterator
*
* @param \RecursiveIterator $iterator
*/
public function __construct(\RecursiveIterator $iterator)
{
parent::__construct($iterator);
if (empty($this::$folder_ignores)) {
$this::$folder_ignores = Grav::instance()['config']->get('system.pages.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 $current \SplFileInfo */
$current = $this->current();
if ($current->isDir() && !in_array($current->getFilename(), $this::$folder_ignores, true)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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()
{
$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;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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)
{
// local cache to speed things up
if (!isset(self::$cache[get_called_class().__METHOD__])) {
self::$cache[get_called_class().__METHOD__] = $items;
}
foreach (self::$cache[get_called_class().__METHOD__] as $name => $item) {
$this->append([$name => $item]);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 {
protected $data;
public function __construct(Data $package, $type = null) {
$this->data = $package;
if ($type) {
$this->data->set('package_type', $type);
}
}
public function getData() {
return $this->data;
}
public function __get($key) {
return $this->data->get($key);
}
public function __isset($key) {
return isset($this->data->$key);
}
public function __toString() {
return $this->toJson();
}
public function toJson() {
return $this->data->toJson();
}
public function toArray() {
return $this->data->toArray();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,533 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 */
const OK = 0;
/** @const Target already exists */
const EXISTS = 1;
/** @const Target is a symbolic link */
const IS_LINK = 2;
/** @const Target doesn't exist */
const NOT_FOUND = 4;
/** @const Target is not a directory */
const NOT_DIRECTORY = 8;
/** @const Target is not a Grav instance */
const NOT_GRAV_ROOT = 16;
/** @const Error while trying to open the ZIP package */
const ZIP_OPEN_ERROR = 32;
/** @const Error while trying to extract the ZIP package */
const ZIP_EXTRACT_ERROR = 64;
/** @const Invalid source file */
const INVALID_SOURCE = 128;
/**
* Destination folder on which validation checks are applied
* @var string
*/
protected static $target;
/**
* @var integer Error Code
*/
protected static $error = 0;
/**
* @var integer 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
* @return bool True if everything went fine, False otherwise.
*/
public static function install($zip, $destination, $options = [], $extracted = null)
{
$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();
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']);
}
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 $zip_file
* @param $destination
* @return bool|string
*/
public static function unZip($zip_file, $destination)
{
$zip = new \ZipArchive();
$archive = $zip->open($zip_file);
if ($archive === true) {
Folder::mkdir($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();
$extracted_folder = $destination . '/' . $package_folder_name;
return $extracted_folder;
}
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 $source_path
* @param $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 $source_path
* @param $install_path
*
* @return bool
*/
public static function copyInstall($source_path, $install_path)
{
if (empty($source_path)) {
throw new \RuntimeException("Directory $source_path is missing");
} else {
Folder::rcopy($source_path, $install_path);
}
return true;
}
/**
* @param $source_path
* @param $install_path
*
* @return bool
*/
public static function sophisticatedInstall($source_path, $install_path, $ignores = [])
{
foreach (new \DirectoryIterator($source_path) as $file) {
if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores)) {
continue;
}
$path = $install_path . DS . $file->getFilename();
if ($file->isDir()) {
Folder::delete($path);
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 boolean 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 boolean 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)) {
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 boolean 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 .= "Malloc 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;
default:
$msg = 'Unknown Error';
break;
}
return $msg;
}
/**
* Returns the last error code of the occurred error
* @return integer 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;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 $slug
* @param $license
*
* @return boolean
*/
public static function set($slug, $license)
{
$licenses = self::getLicenseFile();
$data = $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 $slug
*
* @return string
*/
public static function get($slug = null)
{
$licenses = self::getLicenseFile();
$data = $licenses->content();
$licenses->free();
$slug = strtolower($slug);
if (!$slug) {
return isset($data['licenses']) ? $data['licenses'] : [];
}
if (!isset($data['licenses']) || !isset($data['licenses'][$slug])) {
return '';
}
return $data['licenses'][$slug];
}
/**
* Validates the License format
*
* @param $license
*
* @return bool
*/
public static function validate($license = null)
{
if (!is_string($license)) {
return false;
}
return preg_match('#' . self::$regex. '#', $license);
}
/**
* Get's 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;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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)
{
foreach ($items as $name => $data) {
$data->set('slug', $name);
$this->items[$name] = new Package($data, $this->type);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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->description);
$this->data->set('slug', $package->slug);
$this->data->set('description_html', $html_description);
$this->data->set('description_plain', strip_tags($html_description));
$this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->slug));
}
/**
* @return mixed
*/
public function isEnabled()
{
return $this->settings['enabled'];
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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());
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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());
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 integer
*/
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)
{
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;
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 = isset($this->data['version']) ? $this->data['version'] : '-';
$this->date = isset($this->data['date']) ? $this->data['date'] : '-';
$this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null;
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 (is_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');
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 {
public function __construct($package, $package_type = null) {
$data = new Data($package);
parent::__construct($data, $package_type);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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);
}
}

View File

@@ -0,0 +1,432 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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'])) {
$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 (!Utils::isFunctionDisabled('set_time_limit') && !ini_get('safe_mode') && function_exists('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']) && isset($proxy['pass'])) {
$fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
$overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
$overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
}
}
$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 boolean
*/
public static function isCurlAvailable()
{
return function_exists('curl_version');
}
/**
* Checks if the remote fopen request is enabled in PHP
*
* @return boolean
*/
public static function isFopenAvailable()
{
return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
}
/**
* Is this a remote file or not
*
* @param $file
* @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_array(self::$callback, [$progress]);
}
}
}
}
/**
* Automatically picks the preferred method
*
* @return string The response of the request
*/
private static function getAuto()
{
if (!ini_get('open_basedir') && self::isFopenAvailable()) {
return self::getFopen(func_get_args());
}
if (self::isCurlAvailable()) {
return self::getCurl(func_get_args());
}
return null;
}
/**
* Starts a HTTP request via fopen
*
* @return string The response of the request
*/
private static function getFopen()
{
if (count($args = func_get_args()) == 1) {
$args = $args[0];
}
$uri = $args[0];
$options = $args[1];
$callback = $args[2];
if ($callback) {
$options['fopen']['notification'] = ['self', 'progress'];
}
if (isset($options['fopen']['ssl'])) {
$ssl = $options['fopen']['ssl'];
unset($options['fopen']['ssl']);
$stream = stream_context_create([
'http' => $options['fopen'],
'ssl' => $ssl
], $options['fopen']);
} else {
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
}
$content = @file_get_contents($uri, false, $stream);
if ($content === false) {
$code = null;
if (isset($http_response_header)) {
$code = explode(' ', $http_response_header[0])[1];
}
switch ($code) {
case '404':
throw new \RuntimeException("Page not found");
case '401':
throw new \RuntimeException("Invalid LICENSE");
default:
throw new \RuntimeException("Error while trying to download (code: $code): $uri \n");
}
}
return $content;
}
/**
* Starts a HTTP request via cURL
*
* @return string The response of the request
*/
private static function getCurl()
{
$args = func_get_args();
$args = count($args) > 1 ? $args : array_shift($args);
$uri = $args[0];
$options = $args[1];
$callback = $args[2];
$ch = curl_init($uri);
$response = static::curlExecFollow($ch, $options, $callback);
$errno = curl_errno($ch);
if ($errno) {
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error_message = curl_strerror($errno) . "\n" . curl_error($ch);
switch ($code) {
case '404':
throw new \RuntimeException("Page not found");
case '401':
throw new \RuntimeException("Invalid LICENSE");
default:
throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
}
}
curl_close($ch);
return $response;
}
/**
* @param $ch
* @param $options
* @param $callback
*
* @return bool|mixed
*/
private static function curlExecFollow($ch, $options, $callback)
{
if ($callback) {
curl_setopt_array(
$ch,
[
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
]
);
}
// no open_basedir set, we can proceed normally
if (!ini_get('open_basedir')) {
curl_setopt_array($ch, $options['curl']);
return curl_exec($ch);
}
$max_redirects = isset($options['curl'][CURLOPT_MAXREDIRS]) ? $options['curl'][CURLOPT_MAXREDIRS] : 5;
$options['curl'][CURLOPT_FOLLOWLOCATION] = false;
// open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
curl_setopt_array($ch, $options['curl']);
if ($max_redirects <= 0) {
return curl_exec($ch);
}
$uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$rch = curl_copy_handle($ch);
curl_setopt($rch, CURLOPT_HEADER, true);
curl_setopt($rch, CURLOPT_NOBODY, true);
curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
do {
curl_setopt($rch, CURLOPT_URL, $uri);
$header = curl_exec($rch);
if (curl_errno($rch)) {
$code = 0;
} else {
$code = curl_getinfo($rch, CURLINFO_HTTP_CODE);
if ($code == 301 || $code == 302 || $code == 303) {
preg_match('/Location:(.*?)\n/', $header, $matches);
$uri = trim(array_pop($matches));
} else {
$code = 0;
}
}
} while ($code && --$max_redirects);
curl_close($rch);
if (!$max_redirects) {
if ($max_redirects === null) {
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
}
return false;
}
curl_setopt($ch, CURLOPT_URL, $uri);
return curl_exec($ch);
}
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2015 - 2018 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 (is_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();
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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]);
} else {
return isset($this->{$offset});
}
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return isset($this->{$var}[$offset]) ? $this->{$var}[$offset] : null;
} else {
return isset($this->{$offset}) ? $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;
count($this->{$var});
} else {
count($this->toArray());
}
}
/**
* Returns an associative array of object properties.
*
* @return array
*/
public function toArray()
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
return $this->{$var};
} else {
$properties = (array)$this;
$list = [];
foreach ($properties as $property => $value) {
if ($property[0] != "\0") {
$list[$property] = $value;
}
}
return $list;
}
}
}

View File

@@ -0,0 +1,501 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\DI\Container;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Event\EventDispatcher;
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\LoggerServiceProvider',
'Grav\Common\Service\ErrorServiceProvider',
'uri' => 'Grav\Common\Uri',
'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
'cache' => 'Grav\Common\Cache',
'Grav\Common\Service\SessionServiceProvider',
'plugins' => 'Grav\Common\Plugins',
'themes' => 'Grav\Common\Themes',
'twig' => 'Grav\Common\Twig\Twig',
'taxonomy' => 'Grav\Common\Taxonomy',
'language' => 'Grav\Common\Language\Language',
'pages' => 'Grav\Common\Page\Pages',
'Grav\Common\Service\TaskServiceProvider',
'Grav\Common\Service\AssetsServiceProvider',
'Grav\Common\Service\PageServiceProvider',
'Grav\Common\Service\OutputServiceProvider',
'browser' => 'Grav\Common\Browser',
'exif' => 'Grav\Common\Helpers\Exif',
'Grav\Common\Service\StreamsServiceProvider',
'Grav\Common\Service\ConfigServiceProvider',
'inflector' => 'Grav\Common\Inflector',
'siteSetupProcessor' => 'Grav\Common\Processors\SiteSetupProcessor',
'configurationProcessor' => 'Grav\Common\Processors\ConfigurationProcessor',
'errorsProcessor' => 'Grav\Common\Processors\ErrorsProcessor',
'debuggerInitProcessor' => 'Grav\Common\Processors\DebuggerInitProcessor',
'initializeProcessor' => 'Grav\Common\Processors\InitializeProcessor',
'pluginsProcessor' => 'Grav\Common\Processors\PluginsProcessor',
'themesProcessor' => 'Grav\Common\Processors\ThemesProcessor',
'tasksProcessor' => 'Grav\Common\Processors\TasksProcessor',
'assetsProcessor' => 'Grav\Common\Processors\AssetsProcessor',
'twigProcessor' => 'Grav\Common\Processors\TwigProcessor',
'pagesProcessor' => 'Grav\Common\Processors\PagesProcessor',
'debuggerAssetsProcessor' => 'Grav\Common\Processors\DebuggerAssetsProcessor',
'renderProcessor' => 'Grav\Common\Processors\RenderProcessor',
];
/**
* @var array All processors that are processed in $this->process()
*/
protected $processors = [
'siteSetupProcessor',
'configurationProcessor',
'errorsProcessor',
'debuggerInitProcessor',
'initializeProcessor',
'pluginsProcessor',
'themesProcessor',
'tasksProcessor',
'assetsProcessor',
'twigProcessor',
'pagesProcessor',
'debuggerAssetsProcessor',
'renderProcessor',
];
/**
* 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;
}
/**
* Process a request
*/
public function process()
{
// process all processors (e.g. config, initialize, assets, ..., render)
foreach ($this->processors as $processor) {
$processor = $this[$processor];
$this->measureTime($processor->id, $processor->title, function () use ($processor) {
$processor->process();
});
}
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$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.
*/
public function header()
{
/** @var Page $page */
$page = $this['page'];
$format = $page->templateFormat();
header('Content-type: ' . Utils::getMimeByExtension($format, 'text/html'));
$cache_control = $page->cacheControl();
// Calculate Expires Headers if set to > 0
$expires = $page->expires();
if ($expires > 0) {
$expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
if (!$cache_control) {
header('Cache-Control: max-age=' . $expires);
}
header('Expires: ' . $expires_date);
}
// Set cache-control header
if ($cache_control) {
header('Cache-Control: ' . strtolower($cache_control));
}
// Set the last modified time
if ($page->lastModified()) {
$last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
header('Last-Modified: ' . $last_modified_date);
}
// Calculate a Hash based on the raw file
if ($page->eTag()) {
header('ETag: "' . md5($page->raw() . $page->modified()).'"');
}
// Set HTTP response code
if (isset($this['page']->header()->http_response_code)) {
http_response_code($this['page']->header()->http_response_code);
}
// Vary: Accept-Encoding
if ($this['config']->get('system.pages.vary_accept_encoding', false)) {
header('Vary: Accept-Encoding');
}
}
/**
* 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 like measureTime on the instance.
* Source: http://stackoverflow.com/questions/419804/closures-as-class-members
*/
public function __call($method, $args)
{
$closure = $this->$method;
call_user_func_array($closure, $args);
}
/**
* Initialize and return a Grav instance
*
* @param array $values
*
* @return static
*/
protected static function load(array $values)
{
$container = new static($values);
$container['grav'] = $container;
$container['debugger'] = new Debugger();
$debugger = $container['debugger'];
// closure that measures time by wrapping a function into startTimer and stopTimer
// The debugger can be passed to the closure. Should be more performant
// then to get it from the container all time.
$container->measureTime = function ($timerId, $timerTitle, $callback) use ($debugger) {
$debugger->startTimer($timerId, $timerTitle);
$callback();
$debugger->stopTimer($timerId);
};
$container->measureTime('_services', 'Services', function () use ($container) {
$container->registerServices($container);
});
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->registerServiceProvider($serviceClass);
} else {
$this->registerService($serviceKey, $serviceClass);
}
}
}
/**
* Register a service provider with the container.
*
* @param string $serviceClass
*
* @return void
*/
protected function registerServiceProvider($serviceClass)
{
$this->register(new $serviceClass);
}
/**
* Register a service with the container.
*
* @param string $serviceKey
* @param string $serviceClass
*
* @return void
*/
protected function registerService($serviceKey, $serviceClass)
{
$this[$serviceKey] = function ($c) use ($serviceClass) {
return new $serviceClass($c);
};
}
/**
* This attempts to find media, other files, and download them
*
* @param $path
*/
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 Page $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)) {
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', []))) {
$download = false;
}
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
}
// Nothing found
return false;
}
return $page;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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
*/
public static function getGrav()
{
if (!self::$grav) {
self::$grav = Grav::instance();
}
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
return self::$grav;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
class Base32 {
protected static $base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
protected static $base32Lookup = array(
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 $bytes
* @return string
*/
public static function encode( $bytes ) {
$i = 0; $index = 0; $digit = 0;
$base32 = '';
$bytes_len = strlen($bytes);
while( $i < $bytes_len ) {
$currByte = ord($bytes{$i});
/* Is the current digit going to span a byte boundary? */
if( $index > 3 ) {
if( ($i + 1) < $bytes_len ) {
$nextByte = ord($bytes{$i+1});
} 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 $base32
* @return string
*/
public static function decode( $base32 ) {
$bytes = array();
$base32_len = strlen($base32);
for( $i=$base32_len*5/8-1; $i>=0; --$i ) {
$bytes[] = 0;
}
for( $i = 0, $index = 0, $offset = 0; $i < $base32_len; $i++ ) {
$lookup = ord($base32{$i}) - ord('0');
/* Skip chars outside the lookup table */
if( $lookup < 0 || $lookup >= count(self::$base32Lookup) ) {
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;
}
}

View File

@@ -0,0 +1,362 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use Grav\Common\Page\Page;
use Grav\Common\Uri;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Utils;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts
{
/**
* Process Grav image media URL from HTML tag
*
* @param string $html HTML tag e.g. `<img src="image.jpg" />`
* @param Page $page The current page object
* @return string Returns final HTML string
*/
public static function processImageHtml($html, Page $page)
{
$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 $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 $excerpt
* @param Page $page
* @param string $type
* @return mixed
*/
public static function processLinkExcerpt($excerpt, Page $page, $type = 'link')
{
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
$url_parts = static::parseUrl($url);
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
}, []);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes, true)) {
// support both class and classes.
if ($attrib === 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
}
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
// Handle paths and such.
$url_parts = Uri::convertUrl($page, $url_parts, $type);
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
/**
* Process an image excerpt
*
* @param array $excerpt
* @param Page $page
* @return mixed
*/
public static function processImageExcerpt(array $excerpt, Page $page)
{
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
$url_parts = static::parseUrl($url);
$media = null;
$filename = null;
if (!empty($url_parts['stream'])) {
$filename = $url_parts['scheme'] . '://' . (isset($url_parts['path']) ? $url_parts['path'] : '');
$media = $page->media();
} else {
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
&& (empty($url_parts['host']) || $url_parts['host'] === Grav::instance()['uri']->host());
if ($local_file) {
$filename = basename($url_parts['path']);
$folder = dirname($url_parts['path']);
// Get the local path to page media if possible.
if ($folder === $page->url(false, false, false)) {
// Get the media objects for this page.
$media = $page->media();
} else {
// see if this is an external page to this one
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
/** @var Page $ext_page */
$ext_page = Grav::instance()['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->media();
} else {
Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
}
}
}
}
// If there is a media file that matches the path referenced..
if ($media && $filename && isset($media[$filename])) {
// Get the medium object.
/** @var Medium $medium */
$medium = $media[$filename];
// Process operations
$medium = static::processMediaActions($medium, $url_parts);
$element_excerpt = $excerpt['element']['attributes'];
$alt = isset($element_excerpt['alt']) ? $element_excerpt['alt'] : '';
$title = isset($element_excerpt['title']) ? $element_excerpt['title'] : '';
$class = isset($element_excerpt['class']) ? $element_excerpt['class'] : '';
$id = isset($element_excerpt['id']) ? $element_excerpt['id'] : '';
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
return $excerpt;
}
/**
* Process media actions
*
* @param $medium
* @param $url
* @return mixed
*/
public static function processMediaActions($medium, $url)
{
if (!is_array($url)) {
$url_parts = parse_url($url);
} else {
$url_parts = $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']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? $parts[1] : null;
$carry[] = ['method' => $parts[0], 'params' => $value];
return $carry;
}, []);
}
if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
}
$defaults = Grav::instance()['config']->get('system.images.defaults');
if (is_array($defaults) && count($defaults)) {
foreach ($defaults as $method => $params) {
$actions[] = [
'method' => $method,
'params' => $params,
];
}
}
// loop through actions for the image and call them
foreach ($actions as $action) {
$matches = [];
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $action['params']);
}
$medium = call_user_func_array([$medium, $action['method']], $args);
}
if (isset($url_parts['fragment'])) {
$medium->urlHash($url_parts['fragment']);
}
return $medium;
}
/**
* Variation of parse_url() which works also with local streams.
*
* @param string $url
* @return array|bool
*/
protected static function parseUrl($url)
{
$url_parts = Utils::multibyteParseUrl($url);
if (isset($url_parts['scheme'])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Special handling for the streams.
if ($locator->schemeExists($url_parts['scheme'])) {
if (isset($url_parts['host'])) {
// Merge host and path into a path.
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
unset($url_parts['host']);
}
$url_parts['stream'] = true;
}
}
return $url_parts;
}
protected static function resolveStream($url)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->isStream($url) ? ($locator->findResource($url, false) ?: $locator->findResource($url, false, true)) : $url;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use SebastianBergmann\GlobalState\RuntimeException;
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;
}
}

View File

@@ -0,0 +1,234 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2015 - 2018 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 integer $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;
}
$dom = self::htmlToDomDocument($html);
// Grab the body of our DOM.
$body = $dom->getElementsByTagName("body")->item(0);
// Iterate over words.
$words = new DOMWordsIterator($body);
$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, $body);
if (!empty($ellipsis)) {
self::insertEllipsis($curNode, $ellipsis);
}
$truncated = true;
break;
}
}
// Return original HTML if not truncated.
if ($truncated) {
return self::innerHTML($body);
} else {
return $html;
}
}
/**
* Safely truncates HTML by a given number of letters.
* @param string $html Input HTML.
* @param integer $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;
}
$dom = self::htmlToDomDocument($html);
// Grab the body of our DOM.
$body = $dom->getElementsByTagName('body')->item(0);
// Iterate over letters.
$letters = new DOMLettersIterator($body);
$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], $body);
if (!empty($ellipsis)) {
self::insertEllipsis($currentText[0], $ellipsis);
}
$truncated = true;
break;
}
}
// Return original HTML if not truncated.
if ($truncated) {
return self::innerHTML($body);
} else {
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 = '<p></p>';
}
// 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($html);
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;
}
}
}
/**
* 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;
}
}
/**
* Returns the innerHTML of a particular DOMElement
*
* @param $element
* @return string
*/
private static function innerHTML($element) {
$innerHTML = '';
$children = $element->childNodes;
foreach ($children as $child)
{
$tmp_dom = new DOMDocument();
$tmp_dom->appendChild($tmp_dom->importNode($child, true));
$innerHTML.=trim($tmp_dom->saveHTML());
}
return $innerHTML;
}
}

View File

@@ -0,0 +1,333 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use Grav\Common\Grav;
/**
* This file was originally part of the Akelos Framework
*/
class Inflector
{
protected $plural;
protected $singular;
protected $uncountable;
protected $irregular;
protected $ordinals;
public function init()
{
if (empty($this->plural)) {
$language = Grav::instance()['language'];
$this->plural = $language->translate('INFLECTOR_PLURALS', null, true) ?: [];
$this->singular = $language->translate('INFLECTOR_SINGULAR', null, true) ?: [];
$this->uncountable = $language->translate('INFLECTOR_UNCOUNTABLE', null, true) ?: [];
$this->irregular = $language->translate('INFLECTOR_IRREGULAR', null, true) ?: [];
$this->ordinals = $language->translate('INFLECTOR_ORDINALS', null, true) ?: [];
}
}
/**
* Pluralizes English nouns.
*
* @param string $word English noun to pluralize
* @param int $count The count
*
* @return string Plural noun
*/
public function pluralize($word, $count = 2)
{
$this->init();
if ($count == 1) {
return $word;
}
$lowercased_word = strtolower($word);
foreach ($this->uncountable as $_uncountable) {
if (substr($lowercased_word, (-1 * strlen($_uncountable))) == $_uncountable) {
return $word;
}
}
foreach ($this->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 ($this->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 function singularize($word, $count = 1)
{
$this->init();
if ($count != 1) {
return $word;
}
$lowercased_word = strtolower($word);
foreach ($this->uncountable as $_uncountable) {
if (substr($lowercased_word, (-1 * strlen($_uncountable))) == $_uncountable) {
return $word;
}
}
foreach ($this->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 ($this->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 function titleize($word, $uppercase = '')
{
$uppercase = $uppercase == 'first' ? 'ucfirst' : 'ucwords';
return $uppercase($this->humanize($this->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 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 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 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 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 function variablize($word)
{
$word = $this->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 function tableize($class_name)
{
return $this->pluralize($this->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 function classify($table_name)
{
return $this->camelize($this->singularize($table_name));
}
/**
* Converts number to its ordinal English form.
*
* This method converts 13 to 13th, 2 to 2nd ...
*
* @param integer $number Number to get its ordinal value
*
* @return string Ordinal representation of given string.
*/
public function ordinalize($number)
{
$this->init();
if (in_array(($number % 100), range(11, 13))) {
return $number . $this->ordinals['default'];
} else {
switch (($number % 10)) {
case 1:
return $number . $this->ordinals['first'];
break;
case 2:
return $number . $this->ordinals['second'];
break;
case 3:
return $number . $this->ordinals['third'];
break;
default:
return $number . $this->ordinals['default'];
break;
}
}
}
/**
* Converts a number of days to a number of months
*
* @param int $days
*
* @return int
*/
public 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 = $diff->m + 12 * $diff->y;
}
return $diff->m;
}
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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 (isset($this->items[$key])) ? $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 $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)
{
if ($num > count($this->items)) {
$num = count($this->items);
}
$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 && !call_user_func($callback, $value, $key)) ||
(!$callback && !(bool)$value)
) {
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);
}
}

View File

@@ -0,0 +1,507 @@
<?php
/**
* @package Grav.Common.Language
*
* @copyright Copyright (C) 2015 - 2018 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;
class Language
{
protected $grav;
protected $enabled = true;
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()
{
$this->default = reset($this->languages);
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 $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
sort($languagesArray);
return implode('|', array_reverse($languagesArray));
}
/**
* Gets language, active if set, else default
*
* @return mixed
*/
public function getLanguage()
{
return $this->active ? $this->active : $this->default;
}
/**
* Gets current default language
*
* @return mixed
*/
public function getDefault()
{
return $this->default;
}
/**
* Sets default language manually
*
* @param $lang
*
* @return bool
*/
public function setDefault($lang)
{
if ($this->validate($lang)) {
$this->default = $lang;
return $lang;
}
return false;
}
/**
* Gets current active language
*
* @return mixed
*/
public function getActive()
{
return $this->active;
}
/**
* Sets active language manually
*
* @param $lang
*
* @return 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 $uri
*
* @return mixed
*/
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')) {
$preferred = $this->getBrowserLanguages();
foreach ($preferred as $lang) {
if ($this->validate($lang)) {
$this->active = $lang;
break;
}
}
// Repeat if not found, try base language only - fixes Safari sending the language code always
// with a locale (e.g. it-it or fr-fr).
foreach ($preferred as $lang) {
$lang = substr($lang, 0, 2);
if ($this->validate($lang)) {
$this->active = $lang;
break;
}
}
}
}
}
return $uri;
}
/**
* Get's a URL prefix based on configuration
*
* @param 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 null $lang
* @return bool
*/
public function isIncludeDefaultLanguage($lang = null)
{
// if active lang is not passed in, use current active
if (!$lang) {
$lang = $this->getLanguage();
}
if ($this->default == $lang && $this->config->get('system.languages.include_default_lang') === false) {
return false;
} else {
return true;
}
}
/**
* 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 (empty($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);
unset($valid_lang_extensions[$key]);
array_unshift($valid_lang_extensions, $active_extension);
}
$this->page_extensions = array_merge($valid_lang_extensions, (array)$file_ext);
} 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);
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 $lang
*
* @return bool
*/
public function validate($lang)
{
if (in_array($lang, $this->languages)) {
return true;
}
return false;
}
/**
* Translate a key and possibly arguments into a string using current lang and fallbacks
*
* @param mixed $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);
} else {
return $translation;
}
}
}
}
if ($html_out) {
return '<span class="untranslated">' . $lookup . '</span>';
} else {
return $lookup;
}
}
/**
* Translate Array
*
* @param $key
* @param $index
* @param 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>';
} else {
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
*/
public function getBrowserLanguages($accept_langs = [])
{
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;
}
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;
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* @package Grav.Common.Language
*
* @copyright Copyright (C) 2015 - 2018 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)
{
if (static::getOrientation($code) === 'rtl') {
return true;
}
return false;
}
public static function getNames(array $keys)
{
$results = [];
foreach ($keys as $key) {
if (isset(static::$codes[$key])) {
$results[$key] = static::$codes[$key];
}
}
return $results;
}
protected static function get($code, $type)
{
if (isset(static::$codes[$code][$type])) {
return static::$codes[$code][$type];
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Grav.Common.Markdown
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Markdown;
class Parsedown extends \Parsedown
{
use ParsedownGravTrait;
/**
* Parsedown constructor.
*
* @param $page
* @param $defaults
*/
public function __construct($page, $defaults)
{
$this->init($page, $defaults);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav.Common.Markdown
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Markdown;
class ParsedownExtra extends \ParsedownExtra
{
use ParsedownGravTrait;
/**
* ParsedownExtra constructor.
*
* @param $page
* @param $defaults
* @throws \Exception
*/
public function __construct($page, $defaults)
{
parent::__construct();
$this->init($page, $defaults);
}
}

View File

@@ -0,0 +1,257 @@
<?php
/**
* @package Grav.Common.Markdown
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Markdown;
use Grav\Common\Grav;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait
{
/** @var Page $page */
protected $page;
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 $page
* @param $defaults
*/
protected function init($page, $defaults)
{
$grav = Grav::instance();
$this->page = $page;
$this->BlockTypes['{'] [] = 'TwigTag';
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
if ($defaults === null) {
$defaults = Grav::instance()['config']->get('system.pages.markdown');
}
$this->setBreaksEnabled($defaults['auto_line_breaks']);
$this->setUrlsLinked($defaults['auto_url_links']);
$this->setMarkupEscaped($defaults['escape_markup']);
$this->setSpecialChars($defaults['special_chars']);
$grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this]));
}
/**
* Be able to define a new Block type or override an existing one
*
* @param $type
* @param $tag
* @param bool $continuable
* @param bool $completable
* @param $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 $type
* @param $tag
* @param $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 $Type
*
* @return bool
*/
protected function isBlockContinuable($Type)
{
$continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue');
return $continuable;
}
/**
* Overrides the default behavior to allow for plugin-provided blocks to be completable
*
* @param $Type
*
* @return bool
*/
protected function isBlockCompletable($Type)
{
$completable = \in_array($Type, $this->completable_blocks) || 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 $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' => '&amp;',
'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 = Excerpts::processImageExcerpt($excerpt, $this->page);
}
return $excerpt;
}
protected function inlineLink($excerpt)
{
if (isset($excerpt['type'])) {
$type = $excerpt['type'];
} else {
$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 = Excerpts::processLinkExcerpt($excerpt, $this->page, $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;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media collection interface.
*/
interface MediaCollectionInterface
{
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media interface.
*/
interface MediaInterface
{
/**
* Gets the associated media collection.
*
* @return MediaCollectionInterface Collection of associated media.
*/
public function getMedia();
/**
* Get filesystem path to the associated media.
*
* @return string|null Media path or null if the object doesn't have media folder.
*/
public function getMediaFolder();
/**
* Get display order for the associated media.
*
* @return array Empty array means default ordering.
*/
public function getMediaOrder();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media object interface.
*/
interface MediaObjectInterface
{
}

View File

@@ -0,0 +1,112 @@
<?php
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 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.
*/
abstract public function getMediaOrder();
/**
* 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()
{
$cache = $this->getMediaCache();
if ($this->media === null) {
// Use cached media if possible.
$cacheKey = md5('media' . $this->getCacheKey());
if (!$media = $cache->fetch($cacheKey)) {
$media = new Media($this->getMediaFolder(), $this->getMediaOrder());
$cache->save($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->save($cacheKey, $media);
$this->media = $media;
return $this;
}
/**
* Clear media cache.
*/
protected function clearMediaCache()
{
$cache = $this->getMediaCache();
$cacheKey = md5('media' . $this->getCacheKey());
$cache->delete($cacheKey);
}
/**
* @return Cache
*/
protected function getMediaCache()
{
return Grav::instance()['cache'];
}
/**
* @return string
*/
abstract protected function getCacheKey();
}

View File

@@ -0,0 +1,632 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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\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 Page $page
*
* @return $this
*/
public function addPage(Page $page)
{
$this->items[$page->path()] = ['slug' => $page->slug()];
return $this;
}
/**
* Add a page with path and slug
*
* @param $path
* @param $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 Page
*/
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 !empty($this->items[$offset]) ? $this->pages->get($offset) : null;
}
/**
* Split collection into array of smaller collections.
*
* @param $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 Page|string|null $key
*
* @return $this
* @throws \InvalidArgumentException
*/
public function remove($key = null)
{
if ($key instanceof Page) {
$key = $key->path();
} elseif (is_null($key)) {
$key = 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 boolean True if item is first.
*/
public function isFirst($path)
{
if ($this->items && $path == array_keys($this->items)[0]) {
return true;
} else {
return false;
}
}
/**
* Check to see if this item is the last in the collection.
*
* @param string $path
*
* @return boolean True if item is last.
*/
public function isLast($path)
{
if ($this->items && $path == array_keys($this->items)[count($this->items) - 1]) {
return true;
} else {
return false;
}
}
/**
* Gets the previous sibling based on current position.
*
* @param string $path
*
* @return Page The previous item.
*/
public function prevSibling($path)
{
return $this->adjacentSibling($path, -1);
}
/**
* Gets the next sibling based on current position.
*
* @param string $path
*
* @return Page The next item.
*/
public function nextSibling($path)
{
return $this->adjacentSibling($path, 1);
}
/**
* Returns the adjacent sibling based on a direction.
*
* @param string $path
* @param integer $direction either -1 or +1
*
* @return Page 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 Integer the index of the current page.
*/
public function currentPosition($path)
{
return array_search($path, array_keys($this->items));
}
/**
* 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 $startDate
* @param bool $endDate
* @param $field
*
* @return $this
* @throws \Exception
*/
public function dateRange($startDate, $endDate = false, $field = false)
{
$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 $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 $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)) {
$items[$path] = $slug;
}
}
$this->items = $items;
return $this;
}
/**
* Creates new collection with only pages of one of the specified access levels
*
* @param $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;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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;
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Page\Interfaces;
/**
* Class implements page interface.
*/
interface PageInterface
{
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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;
class Media extends AbstractMedia
{
protected static $global;
protected $path;
protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];
/**
* @param string $path
* @param array $media_order
*/
public function __construct($path, array $media_order = null)
{
$this->path = $path;
$this->media_order = $media_order;
$this->__wakeup();
$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()
{
$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->path)) {
return;
}
$iterator = new \FilesystemIterator($this->path, \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' || $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)) {
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 && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif') && $exif_reader) {
$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);
}
}
/**
* Enable accessing the media path
*
* @return mixed
*/
public function path()
{
return $this->path;
}
}

View File

@@ -0,0 +1,210 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Getters;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Common\Utils;
abstract class AbstractMedia extends Getters implements MediaCollectionInterface
{
protected $gettersVariable = 'instances';
protected $instances = [];
protected $images = [];
protected $videos = [];
protected $audios = [];
protected $files = [];
protected $media_order;
/**
* 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);
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
$object = parent::offsetGet($offset);
// It would be nice if previous image modification would not affect the later ones.
//$object = $object ? clone($object) : null;
return $object;
}
/**
* Get a list of all media.
*
* @return array|MediaObjectInterface[]
*/
public function all()
{
$this->instances = $this->orderMedia($this->instances);
return $this->instances;
}
/**
* Get a list of all image media.
*
* @return array|MediaObjectInterface[]
*/
public function images()
{
$this->images = $this->orderMedia($this->images);
return $this->images;
}
/**
* Get a list of all video media.
*
* @return array|MediaObjectInterface[]
*/
public function videos()
{
$this->videos = $this->orderMedia($this->videos);
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|MediaObjectInterface[]
*/
public function audios()
{
$this->audios = $this->orderMedia($this->audios);
return $this->audios;
}
/**
* Get a list of all file media.
*
* @return array|MediaObjectInterface[]
*/
public function files()
{
$this->files = $this->orderMedia($this->files);
return $this->files;
}
/**
* @param string $name
* @param MediaObjectInterface $file
*/
protected function add($name, $file)
{
$this->instances[$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 $media
* @return array
*/
protected function orderMedia($media)
{
if (null === $this->media_order) {
$page = Grav::instance()['pages']->get($this->path);
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);
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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 boolean $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 $preload
* @return $this
*/
public function preload($preload)
{
$validPreloadAttrs = array('auto','metadata','none');
if (in_array($preload, $validPreloadAttrs))
{
$this->attributes['preload'] = $preload;
}
return $this;
}
/**
* Allows to set the controlsList behaviour
* Separate multiple values with a hyphen
*
* @param $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;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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
{
/**
* @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;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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
*
* @return string
*/
public function cacheFile($type = 'jpg', $quality = 80, $actual = false)
{
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);
// Generates the cache file
$cacheFile = '';
if (!$this->prettyName || $this->prettyPrefix) {
$cacheFile .= $this->hash;
}
if ($this->prettyPrefix) {
$cacheFile .= '-';
}
if ($this->prettyName) {
$cacheFile .= $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);
}
}

View File

@@ -0,0 +1,652 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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 boolean
*/
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'];
if (filesize($this->get('filepath')) === 0) {
return;
}
$image_info = getimagesize($this->get('filepath'));
$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 $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);
$image_dir = $locator->findResource('cache://images', false);
$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)) {
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
}
if ($reset) {
$this->reset();
}
return trim(Grav::instance()['base_url'] . '/' . ltrim($output . $this->querystring() . $this->urlHash(), '/'), '\\');
}
/**
* 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 Inmage's Pretty name stored in cache
*
* @param $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=2500]
* @param int [$step=200]
* @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 boolean $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->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 boolean $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 boolean $reset
* @return Link
*/
public function lightbox($width = null, $height = null, $reset = true)
{
if ($this->mode !== 'source') {
$this->display('source');
}
if ($width && $height) {
$this->cropResize($width, $height);
}
return parent::lightbox($width, $height, $reset);
}
/**
* Sets or gets the quality of the image
*
* @param int $quality 0-100 quality
* @return Medium
*/
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: ![Example](myimg.png?width=200&height=400)
* ![Example](myimg.png?resize=100,200&width=100&height=200)
* ![Example](myimg.png?width=auto&height=auto)
* ![Example](myimg.png?width&height)
* {{ 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: ![Example](myimg.png?width=200&height=400)
* ![Example](myimg.png?resize=100,200&width=100&height=200)
* ![Example](myimg.png?width=auto&height=auto)
* ![Example](myimg.png?width&height)
* {{ 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);
}
/**
* 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;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
class Link implements RenderableInterface
{
use ParsedownHtmlTrait;
/**
* @var array
*/
protected $attributes = [];
protected $source;
/**
* Construct.
* @param array $attributes
* @param Medium $medium
*/
public function __construct(array $attributes, Medium $medium)
{
$this->attributes = $attributes;
$this->source = $medium->reset()->thumbnail('auto')->display('thumbnail');
$this->source->linked = true;
}
/**
* Get an element (is array) that can be rendered by the Parsedown engine
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$innerElement = $this->source->parsedownElement($title, $alt, $class, $id, $reset);
return [
'name' => 'a',
'attributes' => $this->attributes,
'handler' => is_string($innerElement) ? 'line' : 'element',
'text' => $innerElement
];
}
/**
* Forward the call to the source element
*
* @param string $method
* @param mixed $args
* @return mixed
*/
public function __call($method, $args)
{
$this->source = call_user_func_array(array($this->source, $method), $args);
// Don't start nesting links, if user has multiple link calls in his
// actions, we will drop the previous links.
return $this->source instanceof Link ? $this->source : $this;
}
}

View File

@@ -0,0 +1,596 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Common\Data\Blueprint;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
class Medium extends Data implements RenderableInterface, MediaObjectInterface
{
use ParsedownHtmlTrait;
/**
* @var string
*/
protected $mode = 'source';
/**
* @var Medium
*/
protected $_thumbnail = null;
/**
* @var array
*/
protected $thumbnailTypes = [ 'page', 'default' ];
protected $thumbnailType = null;
/**
* @var Medium[]
*/
protected $alternatives = [];
/**
* @var array
*/
protected $attributes = [];
/**
* @var array
*/
protected $styleAttributes = [];
/**
* @var array
*/
protected $metadata = [];
/**
* Construct.
*
* @param array $items
* @param Blueprint $blueprint
*/
public function __construct($items = [], Blueprint $blueprint = null)
{
parent::__construct($items, $blueprint);
if (Grav::instance()['config']->get('system.media.enable_media_timestamp', true)) {
$this->querystring('&' . Grav::instance()['cache']->getKey());
}
$this->def('mime', 'application/octet-stream');
$this->reset();
}
public function __clone()
{
// Allows future compatibility as parent::__clone() works.
}
/**
* Create a copy of this media object
*
* @return Medium
*/
public function copy()
{
return clone $this;
}
/**
* Return just metadata from the Medium object
*
* @return Data
*/
public function meta()
{
return new Data($this->items);
}
/**
* Check if this medium exists or not
*
* @return bool
*/
public function exists()
{
$path = $this->get('filepath');
if (file_exists($path)) {
return true;
}
return false;
}
/**
* Returns an array containing just the metadata
*
* @return array
*/
public function metadata()
{
return $this->metadata;
}
/**
* Add meta file for the medium.
*
* @param $filepath
*/
public function addMetaFile($filepath)
{
$this->metadata = (array)CompiledYamlFile::instance($filepath)->content();
$this->merge($this->metadata);
}
/**
* Add alternative Medium to this Medium.
*
* @param $ratio
* @param Medium $alternative
*/
public function addAlternative($ratio, Medium $alternative)
{
if (!is_numeric($ratio) || $ratio === 0) {
return;
}
$alternative->set('ratio', $ratio);
$width = $alternative->get('width');
$this->alternatives[$width] = $alternative;
}
/**
* Return string representation of the object (html).
*
* @return string
*/
public function __toString()
{
return $this->html();
}
/**
* Return PATH to file.
*
* @param bool $reset
* @return string path to file
*/
public function path($reset = true)
{
if ($reset) {
$this->reset();
}
return $this->get('filepath');
}
/**
* Return the relative path to file
*
* @param bool $reset
* @return mixed
*/
public function relativePath($reset = true)
{
if ($reset) {
$this->reset();
}
return str_replace(GRAV_ROOT, '', $this->get('filepath'));
}
/**
* Return URL to file.
*
* @param bool $reset
* @return string
*/
public function url($reset = true)
{
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
$locator = Grav::instance()['locator'];
if ($locator->isStream($output)) {
$output = $locator->findResource($output, false);
}
if ($reset) {
$this->reset();
}
return trim(Grav::instance()['base_url'] . '/' . ltrim($output . $this->querystring() . $this->urlHash(), '/'), '\\');
}
/**
* Get/set querystring for the file's url
*
* @param string $querystring
* @param boolean $withQuestionmark
* @return string
*/
public function querystring($querystring = null, $withQuestionmark = true)
{
if (!is_null($querystring)) {
$this->set('querystring', ltrim($querystring, '?&'));
foreach ($this->alternatives as $alt) {
$alt->querystring($querystring, $withQuestionmark);
}
}
$querystring = $this->get('querystring', '');
if ($withQuestionmark && !empty($querystring)) {
return '?' . $querystring;
} else {
return $querystring;
}
}
/**
* Get/set hash for the file's url
*
* @param string $hash
* @param boolean $withHash
* @return string
*/
public function urlHash($hash = null, $withHash = true)
{
if ($hash) {
$this->set('urlHash', ltrim($hash, '#'));
}
$hash = $this->get('urlHash', '');
if ($withHash && !empty($hash)) {
return '#' . $hash;
} else {
return $hash;
}
}
/**
* Get an element (is array) that can be rendered by the Parsedown engine
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$attributes = $this->attributes;
$style = '';
foreach ($this->styleAttributes as $key => $value) {
if (is_numeric($key)) // Special case for inline style attributes, refer to style() method
$style .= $value;
else
$style .= $key . ': ' . $value . ';';
}
if ($style) {
$attributes['style'] = $style;
}
if (empty($attributes['title'])) {
if (!empty($title)) {
$attributes['title'] = $title;
} elseif (!empty($this->items['title'])) {
$attributes['title'] = $this->items['title'];
}
}
if (empty($attributes['alt'])) {
if (!empty($alt)) {
$attributes['alt'] = $alt;
} elseif (!empty($this->items['alt'])) {
$attributes['alt'] = $this->items['alt'];
} elseif (!empty($this->items['alt_text'])) {
$attributes['alt'] = $this->items['alt_text'];
} else {
$attributes['alt'] = '';
}
}
if (empty($attributes['class'])) {
if (!empty($class)) {
$attributes['class'] = $class;
} elseif (!empty($this->items['class'])) {
$attributes['class'] = $this->items['class'];
}
}
if (empty($attributes['id'])) {
if (!empty($id)) {
$attributes['id'] = $id;
} elseif (!empty($this->items['id'])) {
$attributes['id'] = $this->items['id'];
}
}
switch ($this->mode) {
case 'text':
$element = $this->textParsedownElement($attributes, false);
break;
case 'thumbnail':
$element = $this->getThumbnail()->sourceParsedownElement($attributes, false);
break;
case 'source':
$element = $this->sourceParsedownElement($attributes, false);
break;
}
if ($reset) {
$this->reset();
}
$this->display('source');
return $element;
}
/**
* Parsedown element for source display mode
*
* @param array $attributes
* @param boolean $reset
* @return array
*/
protected function sourceParsedownElement(array $attributes, $reset = true)
{
return $this->textParsedownElement($attributes, $reset);
}
/**
* Parsedown element for text display mode
*
* @param array $attributes
* @param boolean $reset
* @return array
*/
protected function textParsedownElement(array $attributes, $reset = true)
{
$text = empty($attributes['title']) ? empty($attributes['alt']) ? $this->get('filename') : $attributes['alt'] : $attributes['title'];
$element = [
'name' => 'p',
'attributes' => $attributes,
'text' => $text
];
if ($reset) {
$this->reset();
}
return $element;
}
/**
* Reset medium.
*
* @return $this
*/
public function reset()
{
$this->attributes = [];
return $this;
}
/**
* Switch display mode.
*
* @param string $mode
*
* @return $this
*/
public function display($mode = 'source')
{
if ($this->mode === $mode) {
return $this;
}
$this->mode = $mode;
return $mode === 'thumbnail' ? ($this->getThumbnail() ? $this->getThumbnail()->reset() : null) : $this->reset();
}
/**
* Helper method to determine if this media item has a thumbnail or not
*
* @param string $type;
*
* @return bool
*/
public function thumbnailExists($type = 'page')
{
$thumbs = $this->get('thumbnails');
if (isset($thumbs[$type])) {
return true;
}
return false;
}
/**
* Switch thumbnail.
*
* @param string $type
*
* @return $this
*/
public function thumbnail($type = 'auto')
{
if ($type !== 'auto' && !in_array($type, $this->thumbnailTypes)) {
return $this;
}
if ($this->thumbnailType !== $type) {
$this->_thumbnail = null;
}
$this->thumbnailType = $type;
return $this;
}
/**
* Turn the current Medium into a Link
*
* @param boolean $reset
* @param array $attributes
* @return Link
*/
public function link($reset = true, array $attributes = [])
{
if ($this->mode !== 'source') {
$this->display('source');
}
foreach ($this->attributes as $key => $value) {
empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value;
}
empty($attributes['href']) && $attributes['href'] = $this->url();
return new Link($attributes, $this);
}
/**
* Turn the current Medium into a Link with lightbox enabled
*
* @param int $width
* @param int $height
* @param boolean $reset
* @return Link
*/
public function lightbox($width = null, $height = null, $reset = true)
{
$attributes = ['rel' => 'lightbox'];
if ($width && $height) {
$attributes['data-width'] = $width;
$attributes['data-height'] = $height;
}
return $this->link($reset, $attributes);
}
/**
* Add a class to the element from Markdown or Twig
* Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2)
*
* @return $this
*/
public function classes()
{
$classes = func_get_args();
if (!empty($classes)) {
$this->attributes['class'] = implode(',', (array)$classes);
}
return $this;
}
/**
* Add an id to the element from Markdown or Twig
* Example: ![Example](myimg.png?id=primary-img)
*
* @param $id
* @return $this
*/
public function id($id)
{
if (is_string($id)) {
$this->attributes['id'] = trim($id);
}
return $this;
}
/**
* Allows to add an inline style attribute from Markdown or Twig
* Example: ![Example](myimg.png?style=float:left)
*
* @param string $style
* @return $this
*/
public function style($style)
{
$this->styleAttributes[] = rtrim($style, ';') . ';';
return $this;
}
/**
* Allow any action to be called on this medium from twig or markdown
*
* @param string $method
* @param mixed $args
* @return $this
*/
public function __call($method, $args)
{
$qs = $method;
if (count($args) > 1 || (count($args) == 1 && !empty($args[0]))) {
$qs .= '=' . implode(',', array_map(function ($a) {
if (is_array($a)) {
$a = '[' . implode(',', $a) . ']';
}
return rawurlencode($a);
}, $args));
}
if (!empty($qs)) {
$this->querystring($this->querystring(null, false) . '&' . $qs);
}
return $this;
}
/**
* Get the thumbnail Medium object
*
* @return ThumbnailImageMedium
*/
protected function getThumbnail()
{
if (!$this->_thumbnail) {
$types = $this->thumbnailTypes;
if ($this->thumbnailType !== 'auto') {
array_unshift($types, $this->thumbnailType);
}
foreach ($types as $type) {
$thumb = $this->get('thumbnails.' . $type, false);
if ($thumb) {
$thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
$thumb->parent = $this;
}
if ($thumb) {
$this->_thumbnail = $thumb;
break;
}
}
}
return $this->_thumbnail;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 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\Data\Blueprint;
class MediumFactory
{
/**
* Create Medium from a file
*
* @param string $file
* @param array $params
* @return Medium
*/
public static function fromFile($file, array $params = [])
{
if (!file_exists($file)) {
return null;
}
$parts = pathinfo($file);
$path = $parts['dirname'];
$filename = $parts['basename'];
$ext = $parts['extension'];
$basename = $parts['filename'];
$config = Grav::instance()['config'];
$media_params = $config->get("media.types." . strtolower($ext));
if (!$media_params) {
return null;
}
$params += $media_params;
// Add default settings for undefined variables.
$params += $config->get('media.types.defaults');
$params += [
'type' => 'file',
'thumb' => 'media/thumb.png',
'mime' => 'application/octet-stream',
'filepath' => $file,
'filename' => $filename,
'basename' => $basename,
'extension' => $ext,
'path' => $path,
'modified' => filemtime($file),
'thumbnails' => []
];
$locator = Grav::instance()['locator'];
$file = $locator->findResource("image://{$params['thumb']}");
if ($file) {
$params['thumbnails']['default'] = $file;
}
return static::fromArray($params);
}
/**
* Create Medium from array of parameters
*
* @param array $items
* @param Blueprint|null $blueprint
* @return Medium
*/
public static function fromArray(array $items = [], Blueprint $blueprint = null)
{
$type = isset($items['type']) ? $items['type'] : null;
switch ($type) {
case 'image':
return new ImageMedium($items, $blueprint);
break;
case 'thumbnail':
return new ThumbnailImageMedium($items, $blueprint);
break;
case 'animated':
case 'vector':
return new StaticImageMedium($items, $blueprint);
break;
case 'video':
return new VideoMedium($items, $blueprint);
break;
case 'audio':
return new AudioMedium($items, $blueprint);
break;
default:
return new Medium($items, $blueprint);
break;
}
}
/**
* Create a new ImageMedium by scaling another ImageMedium object.
*
* @param ImageMedium $medium
* @param int $from
* @param int $to
* @return Medium|array
*/
public static function scaledFromMedium($medium, $from, $to)
{
if (! $medium instanceof ImageMedium) {
return $medium;
}
if ($to > $from) {
return $medium;
}
$ratio = $to / $from;
$width = $medium->get('width') * $ratio;
$height = $medium->get('height') * $ratio;
$prev_basename = $medium->get('basename');
$basename = str_replace('@'.$from.'x', '@'.$to.'x', $prev_basename);
$debug = $medium->get('debug');
$medium->set('debug', false);
$medium->setImagePrettyName($basename);
$file = $medium->resize($width, $height)->path();
$medium->set('debug', $debug);
$medium->setImagePrettyName($prev_basename);
$size = filesize($file);
$medium = self::fromFile($file);
if ($medium) {
$medium->set('size', $size);
}
return ['file' => $medium, 'size' => $size];
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Markdown\Parsedown;
trait ParsedownHtmlTrait
{
/**
* @var \Grav\Common\Markdown\Parsedown
*/
protected $parsedown = null;
/**
* Return HTML markup from the medium.
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$element = $this->parsedownElement($title, $alt, $class, $id, $reset);
if (!$this->parsedown) {
$this->parsedown = new Parsedown(null, null);
}
return $this->parsedown->elementToHtml($element);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
interface RenderableInterface
{
/**
* Return HTML markup from the medium.
*
* @param string $title
* @param string $alt
* @param string $class
* @param bool $reset
* @return string
*/
public function html($title = null, $alt = null, $class = null, $reset = true);
/**
* Return Parsedown Element from the medium.
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true);
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
class StaticImageMedium extends Medium
{
use StaticResizeTrait;
/**
* Parsedown element for source display mode
*
* @param array $attributes
* @param boolean $reset
* @return array
*/
protected function sourceParsedownElement(array $attributes, $reset = true)
{
empty($attributes['src']) && $attributes['src'] = $this->url($reset);
return [ 'name' => 'img', 'attributes' => $attributes ];
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
trait StaticResizeTrait
{
/**
* Resize media by setting attributes
*
* @param int $width
* @param int $height
* @return $this
*/
public function resize($width = null, $height = null)
{
$this->styleAttributes['width'] = $width . 'px';
$this->styleAttributes['height'] = $height . 'px';
return $this;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
class ThumbnailImageMedium extends ImageMedium
{
/**
* @var Medium
*/
public $parent = null;
/**
* @var boolean
*/
public $linked = false;
/**
* Return srcset string for this Medium and its alternatives.
*
* @param bool $reset
* @return string
*/
public function srcset($reset = true)
{
return '';
}
/**
* Get an element (is array) that can be rendered by the Parsedown engine
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
return $this->bubble('parsedownElement', [$title, $alt, $class, $id, $reset]);
}
/**
* Return HTML markup from the medium.
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
return $this->bubble('html', [$title, $alt, $class, $id, $reset]);
}
/**
* Switch display mode.
*
* @param string $mode
*
* @return $this
*/
public function display($mode = 'source')
{
return $this->bubble('display', [$mode], false);
}
/**
* Switch thumbnail.
*
* @param string $type
*
* @return $this
*/
public function thumbnail($type = 'auto')
{
$this->bubble('thumbnail', [$type], false);
return $this->bubble('getThumbnail', [], false);
}
/**
* Turn the current Medium into a Link
*
* @param boolean $reset
* @param array $attributes
* @return Link
*/
public function link($reset = true, array $attributes = [])
{
return $this->bubble('link', [$reset, $attributes], false);
}
/**
* Turn the current Medium into a Link with lightbox enabled
*
* @param int $width
* @param int $height
* @param boolean $reset
* @return Link
*/
public function lightbox($width = null, $height = null, $reset = true)
{
return $this->bubble('lightbox', [$width, $height, $reset], false);
}
/**
* Bubble a function call up to either the superclass function or the parent Medium instance
*
* @param string $method
* @param array $arguments
* @param boolean $testLinked
* @return Medium
*/
protected function bubble($method, array $arguments = [], $testLinked = true)
{
if (!$testLinked || $this->linked) {
return $this->parent ? call_user_func_array(array($this->parent, $method), $arguments) : $this;
}
return call_user_func_array(array($this, 'parent::' . $method), $arguments);
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
class VideoMedium extends Medium
{
use StaticResizeTrait;
/**
* Parsedown element for source display mode
*
* @param array $attributes
* @param boolean $reset
* @return array
*/
protected function sourceParsedownElement(array $attributes, $reset = true)
{
$location = $this->url($reset);
return [
'name' => 'video',
'text' => '<source src="' . $location . '">Your browser does not support the video 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 video's poster image
*
* @param $urlImage
* @return $this
*/
public function poster($urlImage)
{
$this->attributes['poster'] = $urlImage;
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;
}
/**
* Allows to set the playsinline attribute
*
* @param bool $status
* @return $this
*/
public function playsinline($status = false)
{
if($status) {
$this->attributes['playsinline'] = true;
} else {
unset($this->attributes['playsinline']);
}
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;
}
/**
* Reset medium.
*
* @return $this
*/
public function reset()
{
parent::reset();
$this->attributes['controls'] = true;
return $this;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
use RocketTheme\Toolbox\ArrayTraits\Constructor;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\Iterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Types implements \ArrayAccess, \Iterator, \Countable
{
use ArrayAccess, Constructor, Iterator, Countable, Export;
protected $items;
protected $systemBlueprints;
public function register($type, $blueprint = null)
{
if (!isset($this->items[$type])) {
$this->items[$type] = [];
} elseif (!$blueprint) {
return;
}
if (!$blueprint && $this->systemBlueprints) {
$blueprint = isset($this->systemBlueprints[$type]) ? $this->systemBlueprints[$type] : $this->systemBlueprints['default'];
}
if ($blueprint) {
array_unshift($this->items[$type], $blueprint);
}
}
public function scanBlueprints($uri)
{
if (!is_string($uri)) {
throw new \InvalidArgumentException('First parameter must be URI');
}
if (!$this->systemBlueprints) {
$this->systemBlueprints = $this->findBlueprints('blueprints://pages');
// Register default by default.
$this->register('default');
$this->register('external');
}
foreach ($this->findBlueprints($uri) as $type => $blueprint) {
$this->register($type, $blueprint);
}
}
public function scanTemplates($uri)
{
if (!is_string($uri)) {
throw new \InvalidArgumentException('First parameter must be URI');
}
$options = [
'compare' => 'Filename',
'pattern' => '|\.html\.twig$|',
'filters' => [
'value' => '|\.html\.twig$|'
],
'value' => 'Filename',
'recursive' => false
];
foreach (Folder::all($uri, $options) as $type) {
$this->register($type);
}
$modular_uri = rtrim($uri, '/') . '/modular';
if (is_dir($modular_uri)) {
foreach (Folder::all($modular_uri, $options) as $type) {
$this->register('modular/' . $type);
}
}
}
public function pageSelect()
{
$list = [];
foreach ($this->items as $name => $file) {
if (strpos($name, '/')) {
continue;
}
$list[$name] = ucfirst(strtr($name, '_', ' '));
}
ksort($list);
return $list;
}
public function modularSelect()
{
$list = [];
foreach ($this->items as $name => $file) {
if (strpos($name, 'modular/') !== 0) {
continue;
}
$list[$name] = trim(ucfirst(strtr(basename($name), '_', ' ')));
}
ksort($list);
return $list;
}
private function findBlueprints($uri)
{
$options = [
'compare' => 'Filename',
'pattern' => '|\.yaml$|',
'filters' => [
'key' => '|\.yaml$|'
],
'key' => 'SubPathName',
'value' => 'PathName',
];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($uri)) {
$options['value'] = 'Url';
}
$list = Folder::all($uri, $options);
return $list;
}
}

View File

@@ -0,0 +1,362 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use Grav\Common\Data\Data;
use Grav\Common\Page\Page;
use Grav\Common\Config\Config;
use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\Event\EventSubscriberInterface;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Exception\LogicException;
class Plugin implements EventSubscriberInterface, \ArrayAccess
{
/**
* @var string
*/
public $name;
/**
* @var array
*/
public $features = [];
/**
* @var Grav
*/
protected $grav;
/**
* @var Config
*/
protected $config;
protected $active = true;
protected $blueprint;
/**
* By default assign all methods as listeners using the default priority.
*
* @return array
*/
public static function getSubscribedEvents()
{
$methods = get_class_methods(get_called_class());
$list = [];
foreach ($methods as $method) {
if (strpos($method, 'on') === 0) {
$list[$method] = [$method, 0];
}
}
return $list;
}
/**
* Constructor.
*
* @param string $name
* @param Grav $grav
* @param Config $config
*/
public function __construct($name, Grav $grav, Config $config = null)
{
$this->name = $name;
$this->grav = $grav;
if ($config) {
$this->setConfig($config);
}
}
/**
* @param Config $config
* @return $this
*/
public function setConfig(Config $config)
{
$this->config = $config;
return $this;
}
/**
* Get configuration of the plugin.
*
* @return array
*/
public function config()
{
return $this->config["plugins.{$this->name}"];
}
/**
* Determine if this is running under the admin
*
* @return bool
*/
public function isAdmin()
{
return Utils::isAdminPlugin();
}
/**
* Determine if this route is in Admin and active for the plugin
*
* @param $plugin_route
* @return bool
*/
protected function isPluginActiveAdmin($plugin_route)
{
$should_run = false;
$uri = $this->grav['uri'];
if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
$should_run = false;
} elseif (isset($uri->paths()[1]) && $uri->paths()[1] === $plugin_route) {
$should_run = true;
}
return $should_run;
}
/**
* @param array $events
*/
protected function enable(array $events)
{
/** @var EventDispatcher $dispatcher */
$dispatcher = $this->grav['events'];
foreach ($events as $eventName => $params) {
if (is_string($params)) {
$dispatcher->addListener($eventName, [$this, $params]);
} elseif (is_string($params[0])) {
$dispatcher->addListener($eventName, [$this, $params[0]], isset($params[1]) ? $params[1] : 0);
} else {
foreach ($params as $listener) {
$dispatcher->addListener($eventName, [$this, $listener[0]], isset($listener[1]) ? $listener[1] : 0);
}
}
}
}
/**
* @param array $events
*/
protected function disable(array $events)
{
/** @var EventDispatcher $dispatcher */
$dispatcher = $this->grav['events'];
foreach ($events as $eventName => $params) {
if (is_string($params)) {
$dispatcher->removeListener($eventName, [$this, $params]);
} elseif (is_string($params[0])) {
$dispatcher->removeListener($eventName, [$this, $params[0]]);
} else {
foreach ($params as $listener) {
$dispatcher->removeListener($eventName, [$this, $listener[0]]);
}
}
}
}
/**
* Whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
$this->loadBlueprint();
if ($offset === 'title') {
$offset = 'name';
}
return isset($this->blueprint[$offset]);
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
$this->loadBlueprint();
if ($offset === 'title') {
$offset = 'name';
}
return isset($this->blueprint[$offset]) ? $this->blueprint[$offset] : null;
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
* @throws LogicException
*/
public function offsetSet($offset, $value)
{
throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
}
/**
* Unsets an offset.
*
* @param mixed $offset The offset to unset.
* @throws LogicException
*/
public function offsetUnset($offset)
{
throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
}
/**
* This function will search a string for markdown links in a specific format. The link value can be
* optionally compared against via the $internal_regex and operated on by the callback $function
* provided.
*
* format: [plugin:myplugin_name](function_data)
*
* @param string $content The string to perform operations upon
* @param callable $function The anonymous callback function
* @param string $internal_regex Optional internal regex to extra data from
*
* @return string
*/
protected function parseLinks($content, $function, $internal_regex = '(.*)')
{
$regex = '/\[plugin:(?:' . $this->name . ')\]\(' . $internal_regex . '\)/i';
return preg_replace_callback($regex, $function, $content);
}
/**
* Merge global and page configurations.
*
* @param Page $page The page to merge the configurations with the
* plugin settings.
* @param mixed $deep false = shallow|true = recursive|merge = recursive+unique
* @param array $params Array of additional configuration options to
* merge with the plugin settings.
* @param string $type Is this 'plugins' or 'themes'
*
* @return Data
*/
protected function mergeConfig(Page $page, $deep = false, $params = [], $type = 'plugins')
{
$class_name = $this->name;
$class_name_merged = $class_name . '.merged';
$defaults = $this->config->get($type . '.' . $class_name, []);
$page_header = $page->header();
$header = [];
if (!isset($page_header->$class_name_merged) && isset($page_header->$class_name)) {
// Get default plugin configurations and retrieve page header configuration
$config = $page_header->$class_name;
if (is_bool($config)) {
// Overwrite enabled option with boolean value in page header
$config = ['enabled' => $config];
}
// Merge page header settings using deep or shallow merging technique
$header = $this->mergeArrays($deep, $defaults, $config);
// Create new config object and set it on the page object so it's cached for next time
$page->modifyHeader($class_name_merged, new Data($header));
} else if (isset($page_header->$class_name_merged)) {
$merged = $page_header->$class_name_merged;
$header = $merged->toArray();
}
if (empty($header)) {
$header = $defaults;
}
// Merge additional parameter with configuration options
$header = $this->mergeArrays($deep, $header, $params);
// Return configurations as a new data config class
return new Data($header);
}
/**
* Merge arrays based on deepness
*
* @param bool $deep
* @param $array1
* @param $array2
* @return array|mixed
*/
private function mergeArrays($deep = false, $array1, $array2)
{
if ($deep === 'merge') {
return Utils::arrayMergeRecursiveUnique($array1, $array2);
}
if ($deep === true) {
return array_replace_recursive($array1, $array2);
}
return array_merge($array1, $array2);
}
/**
* Persists to disk the plugin parameters currently stored in the Grav Config object
*
* @param string $plugin_name The name of the plugin whose config it should store.
*
* @return true
*/
public static function saveConfig($plugin_name)
{
if (!$plugin_name) {
return false;
}
$grav = Grav::instance();
$locator = $grav['locator'];
$filename = 'config://plugins/' . $plugin_name . '.yaml';
$file = YamlFile::instance($locator->findResource($filename, true, true));
$content = $grav['config']->get('plugins.' . $plugin_name);
$file->save($content);
$file->free();
return true;
}
/**
* Simpler getter for the plugin blueprint
*
* @return mixed
*/
public function getBlueprint()
{
if (!$this->blueprint) {
$this->loadBlueprint();
}
return $this->blueprint;
}
/**
* Load blueprints.
*/
protected function loadBlueprint()
{
if (!$this->blueprint) {
$grav = Grav::instance();
$plugins = $grav['plugins'];
$this->blueprint = $plugins->get($this->name)->blueprints();
}
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 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\Data\Blueprints;
use Grav\Common\Data\Data;
use Grav\Common\File\CompiledYamlFile;
use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Plugins extends Iterator
{
public $formFieldTypes;
public function __construct()
{
parent::__construct();
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$iterator = $locator->getIterator('plugins://');
$plugins = [];
foreach($iterator as $directory) {
if (!$directory->isDir()) {
continue;
}
$plugins[] = $directory->getFilename();
}
natsort($plugins);
foreach ($plugins as $plugin) {
$this->add($this->loadPlugin($plugin));
}
}
/**
* @return $this
*/
public function setup()
{
$blueprints = [];
$formFields = [];
/** @var Plugin $plugin */
foreach ($this->items as $plugin) {
if (isset($plugin->features['blueprints'])) {
$blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints'];
}
if (method_exists($plugin, 'getFormFieldTypes')) {
$formFields[get_class($plugin)] = isset($plugin->features['formfields']) ? $plugin->features['formfields'] : 0;
}
}
if ($blueprints) {
// Order by priority.
arsort($blueprints);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$locator->addPath('blueprints', '', array_keys($blueprints), 'system/blueprints');
}
if ($formFields) {
// Order by priority.
arsort($formFields);
$list = [];
foreach ($formFields as $className => $priority) {
$plugin = $this->items[$className];
$list += $plugin->getFormFieldTypes();
}
$this->formFieldTypes = $list;
}
return $this;
}
/**
* Registers all plugins.
*
* @return array|Plugin[] array of Plugin objects
* @throws \RuntimeException
*/
public function init()
{
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
/** @var EventDispatcher $events */
$events = $grav['events'];
foreach ($this->items as $instance) {
// Register only enabled plugins.
if ($config["plugins.{$instance->name}.enabled"] && $instance instanceof Plugin) {
$instance->setConfig($config);
$events->addSubscriber($instance);
}
}
return $this->items;
}
/**
* Add a plugin
*
* @param $plugin
*/
public function add($plugin)
{
if (is_object($plugin)) {
$this->items[get_class($plugin)] = $plugin;
}
}
/**
* Return list of all plugin data with their blueprints.
*
* @return array
*/
public static function all()
{
$plugins = Grav::instance()['plugins'];
$list = [];
foreach ($plugins as $instance) {
$name = $instance->name;
$result = self::get($name);
if ($result) {
$list[$name] = $result;
}
}
return $list;
}
/**
* Get a plugin by name
*
* @param string $name
*
* @return Data|null
*/
public static function get($name)
{
$blueprints = new Blueprints('plugins://');
$blueprint = $blueprints->get("{$name}/blueprints");
// Load default configuration.
$file = CompiledYamlFile::instance("plugins://{$name}/{$name}" . YAML_EXT);
// ensure this is a valid plugin
if (!$file->exists()) {
return null;
}
$obj = new Data($file->content(), $blueprint);
// Override with user configuration.
$obj->merge(Grav::instance()['config']->get('plugins.' . $name) ?: []);
// Save configuration always to user/config.
$file = CompiledYamlFile::instance("config://plugins/{$name}.yaml");
$obj->file($file);
return $obj;
}
protected function loadPlugin($name)
{
$grav = Grav::instance();
$locator = $grav['locator'];
$filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
if (!is_file($filePath)) {
$grav['log']->addWarning(
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name)
);
return null;
}
require_once $filePath;
$pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
$pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name));
}
}
return new $pluginClassName($name, $grav);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
class AssetsProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = 'assets';
public $title = 'Assets';
public function process()
{
$this->container['assets']->init();
$this->container->fireEvent('onAssetsInitialized');
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
class ConfigurationProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = '_config';
public $title = 'Configuration';
public function process()
{
$this->container['config']->init();
$this->container['plugins']->setup();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
class DebuggerAssetsProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = 'debugger_assets';
public $title = 'Debugger Assets';
public function process()
{
$this->container['debugger']->addAssets();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
class DebuggerInitProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = '_debugger';
public $title = 'Init Debugger';
public function process()
{
$this->container['debugger']->init();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
class ErrorsProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = '_errors';
public $title = 'Error Handlers Reset';
public function process()
{
$this->container['errors']->resetHandlers();
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* @package Grav.Common.Processors
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Grav\Common\Config\Config;
use Grav\Common\Uri;
use Grav\Common\Utils;
class InitializeProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = 'init';
public $title = 'Initialize';
public function process()
{
/** @var Config $config */
$config = $this->container['config'];
$config->debug();
// Use output buffering to prevent headers from being sent too early.
ob_start();
if ($config->get('system.cache.gzip') && !@ob_start('ob_gzhandler')) {
// Enable zip/deflate with a fallback in case of if browser does not support compressing.
ob_start();
}
// Initialize the timezone.
if ($config->get('system.timezone')) {
date_default_timezone_set($this->container['config']->get('system.timezone'));
}
// FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
$this->container['session']->init();
}
/** @var Uri $uri */
$uri = $this->container['uri'];
$uri->init();
// Redirect pages with trailing slash if configured to do so.
$path = $uri->path() ?: '/';
if ($path !== '/' && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($path, '/')) {
$this->container->redirectLangSafe(rtrim($path, '/'));
}
$this->container->setLocale();
}
}

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