restored dashboard from usable
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
namespace Grav\Common\Backup;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Inflector;
|
||||
|
||||
/**
|
||||
* The ZipBackup class lets you create simple zip-backups of a grav site
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class ZipBackup
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected static $ignorePaths = [
|
||||
'backup',
|
||||
'cache',
|
||||
'images',
|
||||
'logs'
|
||||
];
|
||||
|
||||
protected static $ignoreFolders = [
|
||||
'.git',
|
||||
'.svn',
|
||||
'.hg',
|
||||
'.idea'
|
||||
];
|
||||
|
||||
/**
|
||||
* Backup
|
||||
*
|
||||
* @param null $destination
|
||||
* @param callable|null $messager
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public static function backup($destination = null, callable $messager = null)
|
||||
{
|
||||
if (!$destination) {
|
||||
$destination = self::getGrav()['locator']->findResource('backup://', true);
|
||||
|
||||
if (!$destination)
|
||||
throw new \RuntimeException('The backup folder is missing.');
|
||||
|
||||
Folder::mkdir($destination);
|
||||
}
|
||||
|
||||
$name = self::getGrav()['config']->get('site.title', basename(GRAV_ROOT));
|
||||
|
||||
$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;
|
||||
} elseif (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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Handles browser and platform versions
|
||||
*
|
||||
* Internally uses the PhpUserAgent package https://github.com/donatj/PhpUserAgent
|
||||
*
|
||||
* @author RocketTHeme
|
||||
* @licence MIT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use \Doctrine\Common\Cache as DoctrineCache;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
*
|
||||
* APC
|
||||
* XCache
|
||||
* RedisCache
|
||||
* MemCache
|
||||
* MemCacheD
|
||||
* FileSystem
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Cache extends Getters
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* @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 $all_remove = [
|
||||
'cache://',
|
||||
'cache://images',
|
||||
'asset://'
|
||||
];
|
||||
|
||||
protected static $assets_remove = [
|
||||
'asset://'
|
||||
];
|
||||
|
||||
protected static $images_remove = [
|
||||
'cache://images'
|
||||
];
|
||||
|
||||
protected static $cache_remove = [
|
||||
'cache://'
|
||||
];
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
$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);
|
||||
|
||||
// Dump Cache state
|
||||
$grav['debugger']->addMessage('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';
|
||||
|
||||
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 'redis':
|
||||
$redis = new \Redis();
|
||||
$redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
|
||||
$this->config->get('system.cache.redis.port', 6379));
|
||||
|
||||
$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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method to get the cache key
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $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 = self::getGrav()['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;
|
||||
default:
|
||||
$remove_paths = self::$standard_remove;
|
||||
}
|
||||
|
||||
|
||||
foreach ($remove_paths as $stream) {
|
||||
|
||||
// Convert stream to a real path
|
||||
$path = $locator->findResource($stream, true, true);
|
||||
// Make sure path exists before proceeding, otherwise we would wipe ROOT_DIR
|
||||
if (!$path) {
|
||||
throw new \RuntimeException("Stream '{$stream}' not found", 500);
|
||||
}
|
||||
|
||||
$anything = false;
|
||||
$files = glob($path . '/*');
|
||||
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $file) {
|
||||
if (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 . '/*';
|
||||
}
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
|
||||
if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
|
||||
touch($user_config);
|
||||
|
||||
$output[] = '<red>Touched: </red>' . $user_config;
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Offers composer helper methods.
|
||||
*
|
||||
* @author eschmar
|
||||
* @license MIT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
|
||||
/**
|
||||
* The Compiled base class.
|
||||
*/
|
||||
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 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->cacheFolder = $cacheFolder;
|
||||
$this->files = $files;
|
||||
$this->path = $path ? rtrim($path, '\\/') . '/' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {}
|
||||
|
||||
/**
|
||||
* 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->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->object->toArray()
|
||||
];
|
||||
|
||||
$file->save($cache);
|
||||
$file->unlock();
|
||||
$file->free();
|
||||
|
||||
$this->modified();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use RocketTheme\Toolbox\Blueprints\Blueprints;
|
||||
|
||||
/**
|
||||
* The Compiled Blueprints class.
|
||||
*/
|
||||
class CompiledBlueprints extends CompiledBase
|
||||
{
|
||||
/**
|
||||
* @var int Version number for the compiled file.
|
||||
*/
|
||||
public $version = 1;
|
||||
|
||||
/**
|
||||
* @var Blueprints Blueprints object.
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* Create configuration object.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
protected function createObject(array $data = [])
|
||||
{
|
||||
$this->object = new Blueprints($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 string $filename File to be loaded.
|
||||
*/
|
||||
protected function loadFile($name, $filename)
|
||||
{
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$this->object->embed($name, $file->content(), '/');
|
||||
$file->free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
|
||||
/**
|
||||
* The Compiled Configuration class.
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Function gets called when cached configuration is saved.
|
||||
*/
|
||||
public function modified()
|
||||
{
|
||||
$this->object->modified(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load single configuration file and append it to the correct position.
|
||||
*
|
||||
* @param string $name Name of the position.
|
||||
* @param string $filename File to be loaded.
|
||||
*/
|
||||
protected function loadFile($name, $filename)
|
||||
{
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$this->object->join($name, $file->content(), '/');
|
||||
$file->free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
|
||||
/**
|
||||
* The Compiled Languages class.
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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($file->content());
|
||||
} else {
|
||||
$this->object->join($name, $file->content(), '/');
|
||||
}
|
||||
$file->free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Service\ConfigServiceProvider;
|
||||
|
||||
/**
|
||||
* The Config class contains configuration information.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Config extends Data
|
||||
{
|
||||
protected $checksum;
|
||||
protected $modified = false;
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @deprecated
|
||||
*/
|
||||
public function getLanguages()
|
||||
{
|
||||
return Grav::instance()['languages'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
|
||||
/**
|
||||
* The Configuration & Blueprints Finder class.
|
||||
*/
|
||||
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->getBasename();
|
||||
$find = ($lookup ?: $name) . '.yaml';
|
||||
$filename = "{$path}/{$name}/{$find}";
|
||||
|
||||
if (file_exists($base . $filename)) {
|
||||
$basename = $this->base . $name;
|
||||
$list[$basename] = ['file' => $filename, 'modified' => filemtime($base . $filename)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects all plugins with a configuration file and returns them with last modification time.
|
||||
*
|
||||
* @param string $folder Location to look up from.
|
||||
* @param string $pattern Pattern to match the file. Pattern will also be removed from the key.
|
||||
* @param int $levels Maximum number of recursive directories.
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
protected function detectAll($folder, $pattern, $levels)
|
||||
{
|
||||
$path = trim(Folder::getRelativePath($folder), '/');
|
||||
|
||||
if (is_dir($folder)) {
|
||||
// Find all system and user configuration files.
|
||||
$options = [
|
||||
'levels' => $levels,
|
||||
'compare' => 'Filename',
|
||||
'pattern' => $pattern,
|
||||
'filters' => [
|
||||
'pre-key' => $this->base,
|
||||
'key' => $pattern,
|
||||
'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
|
||||
return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()];
|
||||
}
|
||||
],
|
||||
'key' => 'SubPathname'
|
||||
];
|
||||
|
||||
$list = Folder::all($folder, $options);
|
||||
|
||||
ksort($list);
|
||||
} else {
|
||||
$list = [];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
|
||||
/**
|
||||
* The Languages class contains configuration rules.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
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 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 = array_merge_recursive($this->items, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
namespace Grav\Common\Config;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Utils;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* The Config class contains configuration information.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Setup extends Data
|
||||
{
|
||||
protected $streams = [
|
||||
'system' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['system'],
|
||||
]
|
||||
],
|
||||
'user' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'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',
|
||||
'prefixes' => [
|
||||
'' => ['cache'],
|
||||
'images' => ['images']
|
||||
]
|
||||
],
|
||||
'log' => [
|
||||
'type' => 'Stream',
|
||||
'prefixes' => [
|
||||
'' => ['logs']
|
||||
]
|
||||
],
|
||||
'backup' => [
|
||||
'type' => 'Stream',
|
||||
'prefixes' => [
|
||||
'' => ['backup']
|
||||
]
|
||||
],
|
||||
'image' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['user://images', 'system://images']
|
||||
]
|
||||
],
|
||||
'page' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['user://pages']
|
||||
]
|
||||
],
|
||||
'account' => [
|
||||
'type' => 'ReadOnlyStream',
|
||||
'prefixes' => [
|
||||
'' => ['user://accounts']
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct($container)
|
||||
{
|
||||
$environment = $container['uri']->environment();
|
||||
if (!$environment) {
|
||||
$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.
|
||||
$file = GRAV_ROOT . '/setup.php';
|
||||
$setup = is_file($file) ? (array) include $file : [];
|
||||
|
||||
// Add default streams defined in beginning of the class.
|
||||
if (!isset($setup['streams']['schemes'])) {
|
||||
$setup['streams']['schemes'] = [];
|
||||
}
|
||||
$setup['streams']['schemes'] += $this->streams;
|
||||
|
||||
// Initialize class.
|
||||
parent::__construct($setup);
|
||||
|
||||
// Set up environment.
|
||||
$this->def('environment', $environment);
|
||||
$this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->environment}"]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
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 = $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
|
||||
*/
|
||||
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']);
|
||||
}
|
||||
if (isset($config['prefixes'])) {
|
||||
foreach ($config['prefixes'] as $prefix => $paths) {
|
||||
$locator->addPath($scheme, $prefix, $paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Export;
|
||||
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
|
||||
|
||||
/**
|
||||
* Blueprint handles the inside logic of blueprints.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Blueprint implements \ArrayAccess, ExportInterface
|
||||
{
|
||||
use Export, NestedArrayAccessWithGetters, GravTrait;
|
||||
|
||||
public $name;
|
||||
|
||||
public $initialized = false;
|
||||
|
||||
protected $items;
|
||||
protected $context;
|
||||
protected $fields;
|
||||
protected $rules = array();
|
||||
protected $nested = array();
|
||||
protected $filter = ['validation' => 1];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @param Blueprints $context
|
||||
*/
|
||||
public function __construct($name, array $data = array(), Blueprints $context = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->items = $data;
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set filter for inherited properties.
|
||||
*
|
||||
* @param array $filter List of field names to be inherited.
|
||||
*/
|
||||
public function setFilter(array $filter)
|
||||
{
|
||||
$this->filter = array_flip($filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all form fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fields()
|
||||
{
|
||||
if (!isset($this->fields)) {
|
||||
$this->fields = [];
|
||||
$this->embed('', $this->items);
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data against blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
{
|
||||
// Initialize data
|
||||
$this->fields();
|
||||
|
||||
try {
|
||||
$this->validateArray($data, $this->nested);
|
||||
} catch (\RuntimeException $e) {
|
||||
$language = self::getGrav()['language'];
|
||||
$message = sprintf($language->translate('FORM.VALIDATION_FAIL', null, true) . ' %s', $e->getMessage());
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two arrays by using blueprints.
|
||||
*
|
||||
* @param array $data1
|
||||
* @param array $data2
|
||||
* @return array
|
||||
*/
|
||||
public function mergeData(array $data1, array $data2)
|
||||
{
|
||||
// Initialize data
|
||||
$this->fields();
|
||||
return $this->mergeArrays($data1, $data2, $this->nested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data by using blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function filter(array $data)
|
||||
{
|
||||
// Initialize data
|
||||
$this->fields();
|
||||
return $this->filterArray($data, $this->nested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data fields that do not exist in blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
public function extra(array $data, $prefix = '')
|
||||
{
|
||||
// Initialize data
|
||||
$this->fields();
|
||||
$rules = $this->nested;
|
||||
|
||||
// Drill down to prefix level
|
||||
if (!empty($prefix)) {
|
||||
$parts = explode('.', trim($prefix, '.'));
|
||||
foreach ($parts as $part) {
|
||||
$rules = isset($rules[$part]) ? $rules[$part] : [];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->extraArray($data, $rules, $prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend blueprint with another blueprint.
|
||||
*
|
||||
* @param Blueprint $extends
|
||||
* @param bool $append
|
||||
*/
|
||||
public function extend(Blueprint $extends, $append = false)
|
||||
{
|
||||
$blueprints = $append ? $this->items : $extends->toArray();
|
||||
$appended = $append ? $extends->toArray() : $this->items;
|
||||
|
||||
$bref_stack = array(&$blueprints);
|
||||
$head_stack = array($appended);
|
||||
|
||||
do {
|
||||
end($bref_stack);
|
||||
|
||||
$bref = &$bref_stack[key($bref_stack)];
|
||||
$head = array_pop($head_stack);
|
||||
|
||||
unset($bref_stack[key($bref_stack)]);
|
||||
|
||||
foreach (array_keys($head) as $key) {
|
||||
if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) {
|
||||
$bref_stack[] = &$bref[$key];
|
||||
$head_stack[] = $head[$key];
|
||||
} else {
|
||||
$bref = array_merge($bref, array($key => $head[$key]));
|
||||
}
|
||||
}
|
||||
} while (count($head_stack));
|
||||
|
||||
$this->items = $blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object into an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
return ['name' => $this->name, 'items' => $this->items, 'rules' => $this->rules, 'nested' => $this->nested];
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed an array to the blueprint.
|
||||
*
|
||||
* @param $name
|
||||
* @param array $value
|
||||
* @param string $separator
|
||||
*/
|
||||
public function embed($name, array $value, $separator = '.')
|
||||
{
|
||||
|
||||
if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
|
||||
return;
|
||||
}
|
||||
// Initialize data
|
||||
$this->fields();
|
||||
$prefix = $name ? strtr($name, $separator, '.') . '.' : '';
|
||||
$params = array_intersect_key($this->filter, $value);
|
||||
$this->parseFormFields($value['form']['fields'], $params, $prefix, $this->fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
protected function validateArray(array $data, array $rules)
|
||||
{
|
||||
$this->checkRequired($data, $rules);
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
$val = isset($rules[$key]) ? $rules[$key] : null;
|
||||
$rule = is_string($val) ? $this->rules[$val] : null;
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
Validation::validate($field, $rule);
|
||||
} elseif (is_array($field) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$this->validateArray($field, $val);
|
||||
} elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
|
||||
// Undefined/extra item.
|
||||
throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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] : null;
|
||||
$rule = is_string($val) ? $this->rules[$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($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
|
||||
$field = null;
|
||||
}
|
||||
|
||||
if (isset($field) && (!is_array($field) || !empty($field))) {
|
||||
$results[$key] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data1
|
||||
* @param array $data2
|
||||
* @param array $rules
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
protected function mergeArrays(array $data1, array $data2, array $rules)
|
||||
{
|
||||
foreach ($data2 as $key => $field) {
|
||||
$val = isset($rules[$key]) ? $rules[$key] : null;
|
||||
$rule = is_string($val) ? $this->rules[$val] : null;
|
||||
|
||||
if (!$rule && array_key_exists($key, $data1) && is_array($field) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
|
||||
} else {
|
||||
// Otherwise just take value from the data2.
|
||||
$data1[$key] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $data1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
protected function extraArray(array $data, array $rules, $prefix)
|
||||
{
|
||||
$array = array();
|
||||
foreach ($data as $key => $field) {
|
||||
$val = isset($rules[$key]) ? $rules[$key] : null;
|
||||
$rule = is_string($val) ? $this->rules[$val] : null;
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
} elseif (is_array($field) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$array += $this->ExtraArray($field, $val, $prefix . $key . '.');
|
||||
} else {
|
||||
// Undefined/extra item.
|
||||
$array[$prefix.$key] = $field;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all field definitions from the blueprints.
|
||||
*
|
||||
* @param array $fields
|
||||
* @param array $params
|
||||
* @param string $prefix
|
||||
* @param array $current
|
||||
* @internal
|
||||
*/
|
||||
protected function parseFormFields(array &$fields, $params, $prefix, array &$current)
|
||||
{
|
||||
// Go though all the fields in current level.
|
||||
foreach ($fields as $key => &$field) {
|
||||
$current[$key] = &$field;
|
||||
// Set name from the array key.
|
||||
$field['name'] = $prefix . $key;
|
||||
$field += $params;
|
||||
|
||||
if (isset($field['fields']) && (!isset($field['type']) || $field['type'] !== 'list')) {
|
||||
// Recursively get all the nested fields.
|
||||
$newParams = array_intersect_key($this->filter, $field);
|
||||
$this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']);
|
||||
} else if ($field['type'] !== 'ignore') {
|
||||
$this->rules[$prefix . $key] = &$field;
|
||||
$this->addProperty($prefix . $key);
|
||||
|
||||
if ($field['type'] === 'list') {
|
||||
// we loop through list to get the actual field
|
||||
if (isset($field['fields']) && $field['fields']) {
|
||||
foreach($field['fields'] as $subName => &$subField) {
|
||||
$this->parseFormField($subField);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->parseFormField($field);
|
||||
}
|
||||
|
||||
if (isset($field['validate']['rule']) && $field['type'] !== 'ignore') {
|
||||
$field['validate'] += $this->getRule($field['validate']['rule']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parses individual field definition
|
||||
*
|
||||
* @param array $field
|
||||
* @internal
|
||||
*/
|
||||
protected function parseFormField(&$field) {
|
||||
foreach ($field as $name => $value) {
|
||||
// Support nested blueprints.
|
||||
if ($this->context && $name == '@import') {
|
||||
$values = (array) $value;
|
||||
if (!isset($field['fields'])) {
|
||||
$field['fields'] = array();
|
||||
}
|
||||
foreach ($values as $bname) {
|
||||
$b = $this->context->get($bname);
|
||||
$field['fields'] = array_merge($field['fields'], $b->fields());
|
||||
}
|
||||
}
|
||||
|
||||
// Support for callable data values.
|
||||
elseif (substr($name, 0, 6) == '@data-') {
|
||||
$property = substr($name, 6);
|
||||
if (is_array($value)) {
|
||||
$func = array_shift($value);
|
||||
} else {
|
||||
$func = $value;
|
||||
$value = array();
|
||||
}
|
||||
list($o, $f) = preg_split('/::/', $func);
|
||||
if (!$f && function_exists($o)) {
|
||||
$data = call_user_func_array($o, $value);
|
||||
} elseif ($f && method_exists($o, $f)) {
|
||||
$data = call_user_func_array(array($o, $f), $value);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elseif (substr($name, 0, 8) == '@config-') {
|
||||
$property = substr($name, 8);
|
||||
$default = isset($field[$property]) ? $field[$property] : null;
|
||||
$config = self::getGrav()['config']->get($value, $default);
|
||||
|
||||
if (!is_null($config)) {
|
||||
$field[$property] = $config;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add property to the definition.
|
||||
*
|
||||
* @param string $path Comma separated path to the property.
|
||||
* @internal
|
||||
*/
|
||||
protected function addProperty($path)
|
||||
{
|
||||
$parts = explode('.', $path);
|
||||
$item = array_pop($parts);
|
||||
|
||||
$nested = &$this->nested;
|
||||
foreach ($parts as $part) {
|
||||
if (!isset($nested[$part])) {
|
||||
$nested[$part] = array();
|
||||
}
|
||||
$nested = &$nested[$part];
|
||||
}
|
||||
|
||||
if (!isset($nested[$item])) {
|
||||
$nested[$item] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $rule
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
protected function getRule($rule)
|
||||
{
|
||||
if (isset($this->items['rules'][$rule]) && is_array($this->items['rules'][$rule])) {
|
||||
return $this->items['rules'][$rule];
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $fields
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
protected function checkRequired(array $data, array $fields)
|
||||
{
|
||||
foreach ($fields as $name => $field) {
|
||||
if (!is_string($field)) {
|
||||
continue;
|
||||
}
|
||||
$field = $this->rules[$field];
|
||||
if (isset($field['validate']['required'])
|
||||
&& $field['validate']['required'] === true
|
||||
&& empty($data[$name])) {
|
||||
$value = isset($field['label']) ? $field['label'] : $field['name'];
|
||||
$language = self::getGrav()['language'];
|
||||
$message = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $value);
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\GravTrait;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* Blueprints class keeps track on blueprint instances.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Blueprints
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected $search;
|
||||
protected $types;
|
||||
protected $instances = array();
|
||||
|
||||
/**
|
||||
* @param string|array $search Search path.
|
||||
*/
|
||||
public function __construct($search)
|
||||
{
|
||||
$this->search = $search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blueprint.
|
||||
*
|
||||
* @param string $type Blueprint type.
|
||||
* @return Blueprint
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function get($type)
|
||||
{
|
||||
if (!isset($this->instances[$type])) {
|
||||
$parents = [];
|
||||
if (is_string($this->search)) {
|
||||
$filename = $this->search . $type . YAML_EXT;
|
||||
|
||||
// Check if search is a stream and resolve the path.
|
||||
if (strpos($filename, '://')) {
|
||||
$grav = static::getGrav();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$parents = $locator->findResources($filename);
|
||||
$filename = array_shift($parents);
|
||||
}
|
||||
} else {
|
||||
$filename = isset($this->search[$type]) ? $this->search[$type] : '';
|
||||
}
|
||||
|
||||
if ($filename && is_file($filename)) {
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
$blueprints = $file->content();
|
||||
} else {
|
||||
$blueprints = [];
|
||||
}
|
||||
|
||||
$blueprint = new Blueprint($type, $blueprints, $this);
|
||||
|
||||
if (isset($blueprints['@extends'])) {
|
||||
// Extend blueprint by other blueprints.
|
||||
$extends = (array) $blueprints['@extends'];
|
||||
|
||||
if (is_string(key($extends))) {
|
||||
$extends = [ $extends ];
|
||||
}
|
||||
|
||||
foreach ($extends as $extendConfig) {
|
||||
$extendType = !is_string($extendConfig) ? empty($extendConfig['type']) ? false : $extendConfig['type'] : $extendConfig;
|
||||
|
||||
if (!$extendType) {
|
||||
continue;
|
||||
} elseif ($extendType === '@parent') {
|
||||
$parentFile = array_shift($parents);
|
||||
if (!$parentFile || !is_file($parentFile)) {
|
||||
continue;
|
||||
}
|
||||
$blueprints = CompiledYamlFile::instance($parentFile)->content();
|
||||
$parent = new Blueprint($type.'-parent', $blueprints, $this);
|
||||
$blueprint->extend($parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($extendConfig) || empty($extendConfig['context'])) {
|
||||
$context = $this;
|
||||
} else {
|
||||
// Load blueprints from external context.
|
||||
$array = explode('://', $extendConfig['context'], 2);
|
||||
$scheme = array_shift($array);
|
||||
$path = array_shift($array);
|
||||
if ($path) {
|
||||
$scheme .= '://';
|
||||
$extendType = $path ? "{$path}/{$extendType}" : $extendType;
|
||||
}
|
||||
$context = new self($scheme);
|
||||
}
|
||||
$blueprint->extend($context->get($extendType));
|
||||
}
|
||||
}
|
||||
|
||||
$this->instances[$type] = $blueprint;
|
||||
}
|
||||
|
||||
return $this->instances[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available blueprint types.
|
||||
*
|
||||
* @return array List of type=>name
|
||||
*/
|
||||
public function types()
|
||||
{
|
||||
if ($this->types === null) {
|
||||
$this->types = array();
|
||||
|
||||
// Check if search is a stream.
|
||||
if (strpos($this->search, '://')) {
|
||||
// Stream: use UniformResourceIterator.
|
||||
$grav = static::getGrav();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$iterator = $locator->getIterator($this->search, null);
|
||||
} else {
|
||||
// Not a stream: use DirectoryIterator.
|
||||
$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(strtr($name, '_', ' '));
|
||||
}
|
||||
}
|
||||
return $this->types;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
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\Blueprints\Blueprints;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
|
||||
/**
|
||||
* Recursive data object
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
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 ? $this->blueprints->extra($this->items) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return blueprints.
|
||||
*
|
||||
* @return Blueprints
|
||||
*/
|
||||
public function blueprints()
|
||||
{
|
||||
if (!$this->blueprints){
|
||||
$this->blueprints = new Blueprints;
|
||||
} elseif (is_callable($this->blueprints)) {
|
||||
// Lazy load blueprints.
|
||||
$blueprints = $this->blueprints;
|
||||
$this->blueprints = $blueprints();
|
||||
}
|
||||
return $this->blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data if storage has been defined.
|
||||
*/
|
||||
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()
|
||||
{
|
||||
return $this->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()
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use RocketTheme\Toolbox\File\FileInterface;
|
||||
|
||||
/**
|
||||
* Data interface
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
interface DataInterface
|
||||
{
|
||||
/**
|
||||
* Get value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @example $value = $data->value('this.is.my.nested.variable');
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $default Default value (or null).
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @return mixed Value.
|
||||
*/
|
||||
public function value($name, $default = null, $separator = '.');
|
||||
|
||||
/**
|
||||
* Merge external data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return mixed
|
||||
*/
|
||||
public function merge(array $data);
|
||||
|
||||
/**
|
||||
* Return blueprints.
|
||||
*/
|
||||
public function blueprints();
|
||||
|
||||
/**
|
||||
* Validate by blueprints.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function validate();
|
||||
|
||||
/**
|
||||
* Filter all items by using blueprints.
|
||||
*/
|
||||
public function filter();
|
||||
|
||||
/**
|
||||
* Get extra items which haven't been defined in blueprints.
|
||||
*/
|
||||
public function extra();
|
||||
|
||||
/**
|
||||
* Save data into the file.
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Set or get the data storage.
|
||||
*
|
||||
* @param FileInterface $storage Optionally enter a new storage.
|
||||
* @return FileInterface
|
||||
*/
|
||||
public function file(FileInterface $storage = null);
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
<?php
|
||||
namespace Grav\Common\Data;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Parser;
|
||||
|
||||
/**
|
||||
* Data validation.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Validation
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* Validate value against a blueprint field definition.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function validate($value, array $field)
|
||||
{
|
||||
$validate = isset($field['validate']) ? (array) $field['validate'] : array();
|
||||
|
||||
// If value isn't required, we will stop validation if empty value is given.
|
||||
if (empty($validate['required']) && ($value === null || $value === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// special case for files, value is never empty and errors with code 4 instead
|
||||
if (empty($validate['required']) && $field['type'] == 'file' && (isset($value['error']) && ($value['error'] == UPLOAD_ERR_NO_FILE || in_array(UPLOAD_ERR_NO_FILE, $value['error'])))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get language class
|
||||
$language = self::getGrav()['language'];
|
||||
|
||||
// Validate type with fallback type text.
|
||||
$type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
|
||||
$method = 'type'.strtr($type, '-', '_');
|
||||
$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 (method_exists(__CLASS__, $method)) {
|
||||
$success = self::$method($value, $validate, $field);
|
||||
} else {
|
||||
$success = self::typeText($value, $validate, $field);
|
||||
}
|
||||
if (!$success) {
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
// Check individual rules
|
||||
foreach ($validate as $rule => $params) {
|
||||
$method = 'validate'.strtr($rule, '-', '_');
|
||||
if (method_exists(__CLASS__, $method)) {
|
||||
$success = self::$method($value, $params);
|
||||
|
||||
if (!$success) {
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] : array();
|
||||
|
||||
// If value isn't required, we will return null if empty value is given.
|
||||
if (empty($validate['required']) && ($value === null || $value === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// special case for files, value is never empty and errors with code 4 instead
|
||||
if (empty($validate['required']) && $field['type'] == 'file' && (isset($value['error']) && ($value['error'] == UPLOAD_ERR_NO_FILE || in_array(UPLOAD_ERR_NO_FILE, $value['error'])))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if this is a YAML field, simply parse it and return the value
|
||||
if (isset($field['yaml']) && $field['yaml'] === true) {
|
||||
try {
|
||||
$yaml = new Parser();
|
||||
return $yaml->parse($value);
|
||||
} catch (ParseException $e) {
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Validate type with fallback type text.
|
||||
$type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
|
||||
$method = 'filter'.strtr($type, '-', '_');
|
||||
if (method_exists(__CLASS__, $method)) {
|
||||
$value = self::$method($value, $validate, $field);
|
||||
} else {
|
||||
$value = self::filterText($value, $validate, $field);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected static function typeCommaList($value, array $params, array $field)
|
||||
{
|
||||
return is_array($value) ? true : self::typeText($value, $params, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
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 ($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)
|
||||
{
|
||||
if (isset($field['multiple']) && $field['multiple'] === true) {
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return reset($value);
|
||||
}
|
||||
|
||||
return $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 (int) $value;
|
||||
}
|
||||
|
||||
protected static function filterDateTime($value, array $params, array $field)
|
||||
{
|
||||
$format = self::getGrav()['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)
|
||||
{
|
||||
return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_string($value)) {
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
return (bool) $params !== true || !empty($value);
|
||||
}
|
||||
|
||||
public static function validatePattern($value, $params)
|
||||
{
|
||||
return (bool) preg_match("`^{$params}$`u", $value);
|
||||
}
|
||||
|
||||
|
||||
// Internal types
|
||||
|
||||
public static function validateAlpha($value, $params)
|
||||
{
|
||||
return ctype_alpha($value);
|
||||
}
|
||||
|
||||
public static function validateAlnum($value, $params)
|
||||
{
|
||||
return ctype_alnum($value);
|
||||
}
|
||||
|
||||
public static function typeBool($value, $params)
|
||||
{
|
||||
return is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
public static function validateBool($value, $params)
|
||||
{
|
||||
return is_bool($value) || $value == 1 || $value == 0;
|
||||
}
|
||||
|
||||
protected static function filterBool($value, $params)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
public static function validateDigit($value, $params)
|
||||
{
|
||||
return ctype_digit($value);
|
||||
}
|
||||
|
||||
public static function validateFloat($value, $params)
|
||||
{
|
||||
return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
|
||||
}
|
||||
|
||||
protected static function filterFloat($value, $params)
|
||||
{
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
public static function validateHex($value, $params)
|
||||
{
|
||||
return ctype_xdigit($value);
|
||||
}
|
||||
|
||||
public static function validateInt($value, $params)
|
||||
{
|
||||
return is_numeric($value) && (int) $value == $value;
|
||||
}
|
||||
|
||||
protected static function filterInt($value, $params)
|
||||
{
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public static function validateArray($value, $params)
|
||||
{
|
||||
return is_array($value) || ($value instanceof \ArrayAccess
|
||||
&& $value instanceof \Traversable
|
||||
&& $value instanceof \Countable);
|
||||
}
|
||||
|
||||
public static function validateJson($value, $params)
|
||||
{
|
||||
return (bool) (json_decode($value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use DebugBar\DataCollector\ConfigCollector;
|
||||
use DebugBar\JavascriptRenderer;
|
||||
use DebugBar\StandardDebugBar;
|
||||
use Grav\Common\Config\Config;
|
||||
|
||||
/**
|
||||
* Class Debugger
|
||||
* @package Grav\Common
|
||||
*/
|
||||
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 = [];
|
||||
|
||||
/**
|
||||
* Debugger constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->debugbar = new StandardDebugBar();
|
||||
$this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the debugger
|
||||
*
|
||||
* @return $this
|
||||
* @throws \DebugBar\DebugBarException
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->grav = Grav::instance();
|
||||
$this->config = $this->grav['config'];
|
||||
|
||||
if ($this->enabled()) {
|
||||
$this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
|
||||
$this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('plugins'), 'Plugins'));
|
||||
}
|
||||
|
||||
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 (isset($state)) {
|
||||
$this->enabled = $state;
|
||||
} else {
|
||||
if (!isset($this->enabled)) {
|
||||
$this->enabled = $this->config->get('system.debugger.enabled');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the debugger assets to the Grav Assets
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addAssets()
|
||||
{
|
||||
if ($this->enabled()) {
|
||||
/** @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 ($css_files as $css) {
|
||||
$assets->addCss($css);
|
||||
}
|
||||
|
||||
$assets->addCss('/system/assets/debugger.css');
|
||||
|
||||
foreach ($js_files as $js) {
|
||||
$assets->addJs($js);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
echo $this->renderer->render();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data through the HTTP headers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendDataInHeaders()
|
||||
{
|
||||
$this->debugbar->sendDataInHeaders();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->config->get('system.debugger.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) && ($name[0] == '_' || $this->config->get('system.debugger.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace Grav\Common\Errors;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Whoops;
|
||||
|
||||
/**
|
||||
* Class Debugger
|
||||
* @package Grav\Common
|
||||
*/
|
||||
class Errors
|
||||
{
|
||||
public function resetHandlers()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$config = $grav['config']->get('system.errors');
|
||||
$jsonRequest = $_SERVER && $_SERVER['HTTP_ACCEPT'] && $_SERVER['HTTP_ACCEPT'] == 'application/json';
|
||||
|
||||
// Setup Whoops-based error handler
|
||||
$whoops = new \Whoops\Run;
|
||||
|
||||
if (isset($config['display'])) {
|
||||
if ($config['display']) {
|
||||
$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);
|
||||
} else {
|
||||
$whoops->pushHandler(new SimplePageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
html, body {
|
||||
height: 100%
|
||||
}
|
||||
body {
|
||||
margin:0 3rem;
|
||||
padding:0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
|
||||
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
|
||||
display: -ms-flexbox; /* TWEENER - IE 10 */
|
||||
display: -webkit-flex; /* NEW - Chrome */
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
margin: 0rem;
|
||||
max-width: 600px;
|
||||
padding-bottom:5rem;
|
||||
}
|
||||
|
||||
header {
|
||||
color: #000;
|
||||
font-size: 4rem;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
p {
|
||||
font-family: Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: bold;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* Layout template file for Whoops's pretty error output.
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Whoops there was an error!</title>
|
||||
<style><?php echo $stylesheet ?></style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="details">
|
||||
<header>
|
||||
Server Error
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<p>Sorry, something went terribly wrong!</p>
|
||||
|
||||
<h3><?php echo $code ?> - <?php echo $message ?></h3>
|
||||
|
||||
<h5>For further details please review your <code>logs/</code> folder, or enable displaying of errors in your system configuration.</h5>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
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();
|
||||
$message = $inspector->getException()->getMessage();
|
||||
|
||||
if ($inspector->getException() instanceof \ErrorException) {
|
||||
$code = Misc::translateErrorCode($code);
|
||||
}
|
||||
|
||||
$vars = array(
|
||||
"stylesheet" => file_get_contents($cssFile),
|
||||
"code" => $code,
|
||||
"message" => $message,
|
||||
);
|
||||
|
||||
$helper->setVariables($vars);
|
||||
$helper->render($templateFile);
|
||||
|
||||
return Handler::QUIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $resource
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
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: " . join(", ", $this->searchPaths). ")"
|
||||
);
|
||||
}
|
||||
|
||||
public function addResourcePath($path)
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"'$path' is not a valid directory"
|
||||
);
|
||||
}
|
||||
|
||||
array_unshift($this->searchPaths, $path);
|
||||
}
|
||||
|
||||
public function getResourcePaths()
|
||||
{
|
||||
return $this->searchPaths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\PhpFile;
|
||||
|
||||
/**
|
||||
* Class CompiledFile
|
||||
* @package Grav\Common\File
|
||||
*
|
||||
* @property string $filename
|
||||
* @property string $extension
|
||||
* @property string $raw
|
||||
* @property array|string $content
|
||||
*/
|
||||
trait CompiledFile
|
||||
{
|
||||
/**
|
||||
* Get/set parsed file contents.
|
||||
*
|
||||
* @param mixed $var
|
||||
* @return string
|
||||
*/
|
||||
public function content($var = null)
|
||||
{
|
||||
// Set some options
|
||||
$this->settings(['native' => true, 'compat' => true]);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
$file->free();
|
||||
|
||||
$this->content = $cache['data'];
|
||||
}
|
||||
|
||||
return parent::content($var);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\MarkdownFile;
|
||||
|
||||
class CompiledMarkdownFile extends MarkdownFile
|
||||
{
|
||||
use CompiledFile;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace Grav\Common\File;
|
||||
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
|
||||
class CompiledYamlFile extends YamlFile
|
||||
{
|
||||
use CompiledFile;
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
/**
|
||||
* Folder helper class.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
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;
|
||||
|
||||
$directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$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;
|
||||
|
||||
$directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $filepath => $file) {
|
||||
$file_modified = $file->getMTime();
|
||||
if ($file_modified > $last_modified) {
|
||||
$last_modified = $file_modified;
|
||||
}
|
||||
}
|
||||
|
||||
return $last_modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 to {$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;
|
||||
|
||||
if ($recursive) {
|
||||
$directory = new \RecursiveDirectoryIterator($path,
|
||||
\RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF);
|
||||
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$iterator->setMaxDepth(max($levels, -1));
|
||||
} 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 (!is_dir($source)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Make sure that path to the target exists before moving.
|
||||
self::create(dirname($target));
|
||||
|
||||
// Just rename the directory.
|
||||
$success = @rename($source, $target);
|
||||
|
||||
if (!$success) {
|
||||
$error = error_get_last();
|
||||
throw new \RuntimeException($error['message']);
|
||||
}
|
||||
|
||||
// Make sure that the change will be detected when caching.
|
||||
@touch(dirname($source));
|
||||
@touch(dirname($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete directory from filesystem.
|
||||
*
|
||||
* @param string $target
|
||||
* @param bool $include_target
|
||||
* @return bool
|
||||
*/
|
||||
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
|
||||
* @internal
|
||||
*/
|
||||
public static function mkdir($folder)
|
||||
{
|
||||
self::create($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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)) {
|
||||
if (!mkdir($dest)) {
|
||||
// If the destination directory could not be created stop processing
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
/** @var \DirectoryIterator $fileinfo */
|
||||
foreach ($files as $fileinfo) {
|
||||
if ($fileinfo->isDir()) {
|
||||
if (false === @rmdir($fileinfo->getRealPath())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (false === @unlink($fileinfo->getRealPath())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $include_target ? @rmdir($folder) : true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
|
||||
/**
|
||||
* Class RecursiveFolderFilterIterator
|
||||
* @package Grav\Common\Filesystem
|
||||
*/
|
||||
class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
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 = self::getGrav()['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)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
abstract class AbstractCollection extends Iterator {
|
||||
|
||||
use GravTrait;
|
||||
|
||||
public function toJson()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return json_encode($items);
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Common;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Iterator;
|
||||
|
||||
abstract class AbstractPackageCollection extends Iterator {
|
||||
|
||||
use GravTrait;
|
||||
|
||||
protected $type;
|
||||
|
||||
public function toJson()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return json_encode($items);
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $name => $package) {
|
||||
$items[$name] = $package->toArray();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Inflector;
|
||||
use Grav\Common\Iterator;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
class GPM extends Iterator
|
||||
{
|
||||
/**
|
||||
* Local installed Packages
|
||||
* @var Local\Packages
|
||||
*/
|
||||
private $installed;
|
||||
|
||||
/**
|
||||
* Remote available Packages
|
||||
* @var Remote\Packages
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var Remote\Grav
|
||||
*/
|
||||
public $grav;
|
||||
|
||||
/**
|
||||
* Internal cache
|
||||
* @var
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
protected $install_paths = ['plugins' => 'user/plugins/%name%', 'themes' => 'user/themes/%name%', 'skeletons' => 'user/'];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$this->installed = new Local\Packages();
|
||||
try {
|
||||
$this->repository = new Remote\Packages($refresh, $callback);
|
||||
$this->grav = new Remote\Grav($refresh, $callback);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Locally installed packages
|
||||
* @return Iterator The installed packages
|
||||
*/
|
||||
public function getInstalled()
|
||||
{
|
||||
return $this->installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of locally installed packages
|
||||
* @return integer Amount of installed packages
|
||||
*/
|
||||
public function countInstalled()
|
||||
{
|
||||
$installed = $this->getInstalled();
|
||||
|
||||
return count($installed['plugins']) + count($installed['themes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the instance of a specific Plugin
|
||||
* @param string $slug The slug of the Plugin
|
||||
* @return Local\Package The instance of the Plugin
|
||||
*/
|
||||
public function getInstalledPlugin($slug)
|
||||
{
|
||||
return $this->installed['plugins'][$slug];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Locally installed plugins
|
||||
* @return Iterator The installed plugins
|
||||
*/
|
||||
public function getInstalledPlugins()
|
||||
{
|
||||
return $this->installed['plugins'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Plugin is installed
|
||||
* @param string $slug The slug of the Plugin
|
||||
* @return boolean True if the Plugin has been installed. False otherwise
|
||||
*/
|
||||
public function isPluginInstalled($slug)
|
||||
{
|
||||
return isset($this->installed['plugins'][$slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the instance of a specific Theme
|
||||
* @param string $slug The slug of the Theme
|
||||
* @return Local\Package The instance of the Theme
|
||||
*/
|
||||
public function getInstalledTheme($slug)
|
||||
{
|
||||
return $this->installed['themes'][$slug];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Locally installed themes
|
||||
* @return Iterator The installed themes
|
||||
*/
|
||||
public function getInstalledThemes()
|
||||
{
|
||||
return $this->installed['themes'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Theme is installed
|
||||
* @param string $slug The slug of the Theme
|
||||
* @return boolean True if the Theme has been installed. False otherwise
|
||||
*/
|
||||
public function isThemeInstalled($slug)
|
||||
{
|
||||
return isset($this->installed['themes'][$slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of updates available
|
||||
* @return integer Amount of available updates
|
||||
*/
|
||||
public function countUpdates()
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
$count += count($this->getUpdatablePlugins());
|
||||
$count += count($this->getUpdatableThemes());
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Plugins and Themes that can be updated.
|
||||
* Plugins and Themes are extended with the `available` property that relies to the remote version
|
||||
* @return array Array of updatable Plugins and Themes.
|
||||
* Format: ['total' => int, 'plugins' => array, 'themes' => array]
|
||||
*/
|
||||
public function getUpdatable()
|
||||
{
|
||||
$plugins = $this->getUpdatablePlugins();
|
||||
$themes = $this->getUpdatableThemes();
|
||||
|
||||
$items = [
|
||||
'total' => count($plugins)+count($themes),
|
||||
'plugins' => $plugins,
|
||||
'themes' => $themes
|
||||
];
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Plugins that can be updated.
|
||||
* The Plugins are extended with the `available` property that relies to the remote version
|
||||
* @return Iterator Array of updatable Plugins
|
||||
*/
|
||||
public function getUpdatablePlugins()
|
||||
{
|
||||
$items = [];
|
||||
$repository = $this->repository['plugins'];
|
||||
|
||||
// local cache to speed things up
|
||||
if (isset($this->cache[__METHOD__])) {
|
||||
return $this->cache[__METHOD__];
|
||||
}
|
||||
|
||||
foreach ($this->installed['plugins'] as $slug => $plugin) {
|
||||
if (!isset($repository[$slug]) || $plugin->symlink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$local_version = $plugin->version ? $plugin->version : 'Unknown';
|
||||
$remote_version = $repository[$slug]->version;
|
||||
|
||||
if (version_compare($local_version, $remote_version) < 0) {
|
||||
$repository[$slug]->available = $remote_version;
|
||||
$repository[$slug]->version = $local_version;
|
||||
$items[$slug] = $repository[$slug];
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache[__METHOD__] = $items;
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Plugin or Theme is updatable
|
||||
* @param string $slug The slug of the package
|
||||
* @return boolean True if updatable. False otherwise or if not found
|
||||
*/
|
||||
public function isUpdatable($slug)
|
||||
{
|
||||
return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Plugin is updatable
|
||||
* @param string $plugin The slug of the Plugin
|
||||
* @return boolean True if the Plugin is updatable. False otherwise
|
||||
*/
|
||||
public function isPluginUpdatable($plugin)
|
||||
{
|
||||
return array_key_exists($plugin, (array) $this->getUpdatablePlugins());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Themes that can be updated.
|
||||
* The Themes are extended with the `available` property that relies to the remote version
|
||||
* @return Iterator Array of updatable Themes
|
||||
*/
|
||||
public function getUpdatableThemes()
|
||||
{
|
||||
$items = [];
|
||||
$repository = $this->repository['themes'];
|
||||
|
||||
// local cache to speed things up
|
||||
if (isset($this->cache[__METHOD__])) {
|
||||
return $this->cache[__METHOD__];
|
||||
}
|
||||
|
||||
foreach ($this->installed['themes'] as $slug => $plugin) {
|
||||
if (!isset($repository[$slug]) || $plugin->symlink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$local_version = $plugin->version ? $plugin->version : 'Unknown';
|
||||
$remote_version = $repository[$slug]->version;
|
||||
|
||||
if (version_compare($local_version, $remote_version) < 0) {
|
||||
$repository[$slug]->available = $remote_version;
|
||||
$repository[$slug]->version = $local_version;
|
||||
$items[$slug] = $repository[$slug];
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache[__METHOD__] = $items;
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Theme is Updatable
|
||||
* @param string $theme The slug of the Theme
|
||||
* @return boolean True if the Theme is updatable. False otherwise
|
||||
*/
|
||||
public function isThemeUpdatable($theme)
|
||||
{
|
||||
return array_key_exists($theme, (array) $this->getUpdatableThemes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Plugin from the repository
|
||||
* @param string $slug The slug of the Plugin
|
||||
* @return mixed Package if found, NULL if not
|
||||
*/
|
||||
public function getRepositoryPlugin($slug)
|
||||
{
|
||||
return @$this->repository['plugins'][$slug];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Plugins available in the repository
|
||||
* @return Iterator The Plugins remotely available
|
||||
*/
|
||||
public function getRepositoryPlugins()
|
||||
{
|
||||
return $this->repository['plugins'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Theme from the repository
|
||||
* @param string $slug The slug of the Theme
|
||||
* @return mixed Package if found, NULL if not
|
||||
*/
|
||||
public function getRepositoryTheme($slug)
|
||||
{
|
||||
return @$this->repository['themes'][$slug];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Themes available in the repository
|
||||
* @return Iterator The Themes remotely available
|
||||
*/
|
||||
public function getRepositoryThemes()
|
||||
{
|
||||
return $this->repository['themes'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Plugins and Themes available in the repository
|
||||
* @return array Array of available Plugins and Themes
|
||||
* Format: ['plugins' => array, 'themes' => array]
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a Package in the repository
|
||||
* @param string $search Can be either the slug or the name
|
||||
* @return Remote\Package Package if found, FALSE if not
|
||||
*/
|
||||
public function findPackage($search)
|
||||
{
|
||||
$search = strtolower($search);
|
||||
if ($found = $this->getRepositoryTheme($search)) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
if ($found = $this->getRepositoryPlugin($search)) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
$themes = $this->getRepositoryThemes();
|
||||
$plugins = $this->getRepositoryPlugins();
|
||||
|
||||
if (!$themes && !$plugins) {
|
||||
if (!is_writable(ROOT_DIR . '/cache/gpm')) {
|
||||
throw new \RuntimeException("The cache/gpm folder is not writable. Please check the folder permissions.");
|
||||
}
|
||||
|
||||
throw new \RuntimeException("GPM not reachable. Please check your internet connection or check the Grav site is reachable");
|
||||
}
|
||||
|
||||
if ($themes) foreach ($themes as $slug => $theme) {
|
||||
if ($search == $slug || $search == $theme->name) {
|
||||
return $theme;
|
||||
}
|
||||
}
|
||||
|
||||
if ($plugins) foreach ($plugins as $slug => $plugin) {
|
||||
if ($search == $slug || $search == $plugin->name) {
|
||||
return $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Plugins and Themes available in the repository
|
||||
* @return array Array of available Plugins and Themes
|
||||
* Format: ['plugins' => array, 'themes' => array]
|
||||
*/
|
||||
/**
|
||||
* Searches for a list of Packages in the repository
|
||||
* @param array $searches An array of either slugs or names
|
||||
* @return array Array of found Packages
|
||||
* Format: ['total' => int, 'not_found' => array, <found-slugs>]
|
||||
*/
|
||||
public function findPackages($searches = [])
|
||||
{
|
||||
$packages = ['total' => 0, 'not_found' => []];
|
||||
$inflector = new Inflector();
|
||||
|
||||
foreach ($searches as $search) {
|
||||
$repository = '';
|
||||
// if this is an object, get the search data from the key
|
||||
if (is_object($search)) {
|
||||
$search = (array) $search;
|
||||
$key = key($search);
|
||||
$repository = $search[$key];
|
||||
$search = $key;
|
||||
}
|
||||
|
||||
if ($found = $this->findPackage($search)) {
|
||||
// set override repository if provided
|
||||
if ($repository) {
|
||||
$found->override_repository = $repository;
|
||||
}
|
||||
if (!isset($packages[$found->package_type])) {
|
||||
$packages[$found->package_type] = [];
|
||||
}
|
||||
|
||||
$packages[$found->package_type][$found->slug] = $found;
|
||||
$packages['total']++;
|
||||
} else {
|
||||
// make a best guess at the type based on the repo URL
|
||||
if (Utils::contains($repository, '-theme')) {
|
||||
$type = 'themes';
|
||||
} else {
|
||||
$type = 'plugins';
|
||||
}
|
||||
|
||||
$not_found = new \stdClass();
|
||||
$not_found->name = $inflector->camelize($search);
|
||||
$not_found->slug = $search;
|
||||
$not_found->package_type = $type;
|
||||
$not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]);
|
||||
$not_found->override_repository = $repository;
|
||||
$packages['not_found'][$search] = $not_found;
|
||||
}
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Destination folder on which validation checks are applied
|
||||
* @var string
|
||||
*/
|
||||
protected static $target;
|
||||
|
||||
/**
|
||||
* Error Code
|
||||
* @var integer
|
||||
*/
|
||||
protected static $error = 0;
|
||||
|
||||
/**
|
||||
* Default options for the install
|
||||
* @var array
|
||||
*/
|
||||
protected static $options = [
|
||||
'overwrite' => true,
|
||||
'ignore_symlinks' => true,
|
||||
'sophisticated' => false,
|
||||
'theme' => false,
|
||||
'install_path' => '',
|
||||
'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK]
|
||||
];
|
||||
|
||||
/**
|
||||
* Installs a given package to a given destination.
|
||||
*
|
||||
* @param string $package The local path to the 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']
|
||||
*
|
||||
* @return boolean True if everything went fine, False otherwise.
|
||||
*/
|
||||
public static function install($package, $destination, $options = [])
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
// Pre install checks
|
||||
static::flightProcessing('pre_install', $install_path);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
$archive = $zip->open($package);
|
||||
$tmp = CACHE_DIR . 'tmp/Grav-' . uniqid();
|
||||
|
||||
if ($archive !== true) {
|
||||
self::$error = self::ZIP_OPEN_ERROR;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Folder::mkdir($tmp);
|
||||
|
||||
$unzip = $zip->extractTo($tmp);
|
||||
|
||||
if (!$unzip) {
|
||||
self::$error = self::ZIP_EXTRACT_ERROR;
|
||||
|
||||
$zip->close();
|
||||
Folder::delete($tmp);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!$options['sophisticated']) {
|
||||
if ($options['theme']) {
|
||||
self::copyInstall($zip, $install_path, $tmp);
|
||||
} else {
|
||||
self::moveInstall($zip, $install_path, $tmp);
|
||||
}
|
||||
} else {
|
||||
self::sophisticatedInstall($zip, $install_path, $tmp);
|
||||
}
|
||||
|
||||
Folder::delete($tmp);
|
||||
$zip->close();
|
||||
|
||||
// Post install checks
|
||||
static::flightProcessing('post_install', $install_path);
|
||||
|
||||
self::$error = self::OK;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $state
|
||||
* @param $install_path
|
||||
*/
|
||||
protected static function flightProcessing($state, $install_path)
|
||||
{
|
||||
$blueprints_path = $install_path . DS . 'blueprints.yaml';
|
||||
|
||||
if (file_exists($blueprints_path)) {
|
||||
$package_yaml = Yaml::parse(file_get_contents($blueprints_path));
|
||||
if (isset($package_yaml['install'][$state]['create'])) {
|
||||
foreach ((array) $package_yaml['install']['pre_install']['create'] as $file) {
|
||||
Folder::mkdir($install_path . '/' . ltrim($file, '/'));
|
||||
}
|
||||
}
|
||||
if (isset($package_yaml['install'][$state]['remove'])) {
|
||||
foreach ((array) $package_yaml['install']['pre_install']['remove'] as $file) {
|
||||
Folder::delete($install_path . '/' . ltrim($file, '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ZipArchive $zip
|
||||
* @param $install_path
|
||||
* @param $tmp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function moveInstall(\ZipArchive $zip, $install_path, $tmp)
|
||||
{
|
||||
$container = $zip->getNameIndex(0);
|
||||
if (file_exists($install_path)) {
|
||||
Folder::delete($install_path);
|
||||
}
|
||||
|
||||
Folder::move($tmp . DS . $container, $install_path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ZipArchive $zip
|
||||
* @param $install_path
|
||||
* @param $tmp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function copyInstall(\ZipArchive $zip, $install_path, $tmp)
|
||||
{
|
||||
$firstDir = $zip->getNameIndex(0);
|
||||
if (empty($firstDir)) {
|
||||
throw new \RuntimeException("Directory $firstDir is missing");
|
||||
} else {
|
||||
$tmp = realpath($tmp . DS . $firstDir);
|
||||
Folder::rcopy($tmp, $install_path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ZipArchive $zip
|
||||
* @param $install_path
|
||||
* @param $tmp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp)
|
||||
{
|
||||
for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) {
|
||||
$filename = $zip->getNameIndex($i);
|
||||
$fileinfo = pathinfo($filename);
|
||||
$depth = count(explode(DS, rtrim($filename, '/')));
|
||||
|
||||
if ($depth > 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $install_path . DS . $fileinfo['basename'];
|
||||
|
||||
if (is_link($path)) {
|
||||
continue;
|
||||
} else {
|
||||
if (is_dir($path)) {
|
||||
Folder::delete($path);
|
||||
Folder::move($tmp . DS . $filename, $path);
|
||||
|
||||
if ($fileinfo['basename'] == 'bin') {
|
||||
foreach (glob($path . DS . '*') as $file) {
|
||||
@chmod($file, 0755);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@unlink($path);
|
||||
@copy($tmp . DS . $filename, $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;
|
||||
}
|
||||
|
||||
return Folder::delete($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 = 'An error occurred while extracting the package';
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
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->name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->settings['enabled'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Local;
|
||||
|
||||
use Grav\Common\GPM\Common\CachedCollection;
|
||||
|
||||
class Packages extends CachedCollection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$items = [
|
||||
'plugins' => new Plugins(),
|
||||
'themes' => new Themes()
|
||||
];
|
||||
|
||||
parent::__construct($items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Local;
|
||||
|
||||
/**
|
||||
* Class Plugins
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'plugins';
|
||||
|
||||
/**
|
||||
* Local Plugins Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::getGrav()['plugins']->all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Local;
|
||||
|
||||
/**
|
||||
* Class Themes
|
||||
* @package Grav\Common\GPM\Local
|
||||
*/
|
||||
class Themes extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'themes';
|
||||
|
||||
/**
|
||||
* Local Themes Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::getGrav()['themes']->all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
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;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
$cache_dir = self::getGrav()['locator']->findResource('cache://gpm', true, true);
|
||||
$this->cache = new FilesystemCache($cache_dir);
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->raw = $this->cache->fetch(md5($this->repository));
|
||||
|
||||
$this->fetch($refresh, $callback);
|
||||
foreach (json_decode($this->raw, true) as $slug => $data) {
|
||||
$this->items[$slug] = new Package($data, $this->type);
|
||||
}
|
||||
}
|
||||
|
||||
public function fetch($refresh = false, $callback = null)
|
||||
{
|
||||
if (!$this->raw || $refresh) {
|
||||
$response = Response::get($this->repository, [], $callback);
|
||||
$this->raw = $response;
|
||||
$this->cache->save(md5($this->repository), $this->raw, $this->lifetime);
|
||||
}
|
||||
|
||||
return $this->raw;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
use \Doctrine\Common\Cache\FilesystemCache;
|
||||
|
||||
class Grav extends AbstractPackageCollection
|
||||
{
|
||||
protected $repository = 'https://getgrav.org/downloads/grav.json';
|
||||
private $data;
|
||||
|
||||
private $version;
|
||||
private $date;
|
||||
|
||||
/**
|
||||
* @param bool $refresh
|
||||
* @param null $callback
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$cache_dir = self::getGrav()['locator']->findResource('cache://gpm', true, true);
|
||||
$this->cache = new FilesystemCache($cache_dir);
|
||||
$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'] : '-';
|
||||
|
||||
if (isset($this->data['assets'])) foreach ($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 ($this->data['changelog'] as $version => $changelog) {
|
||||
preg_match("/[\w-\.]+/", $version, $cleanVersion);
|
||||
|
||||
if (!$cleanVersion || version_compare($diff, $cleanVersion[0], ">=")) { continue; }
|
||||
|
||||
$diffLog[$version] = $changelog;
|
||||
}
|
||||
|
||||
return $diffLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest version of Grav available remotely
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the release date of the latest Grav
|
||||
* @return string
|
||||
*/
|
||||
public function getDate()
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function isUpdatable()
|
||||
{
|
||||
return version_compare(GRAV_VERSION, $this->getVersion(), '<');
|
||||
}
|
||||
|
||||
public function isSymlink()
|
||||
{
|
||||
return is_link(GRAV_ROOT . DS . 'index.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
use Grav\Common\GPM\Common\CachedCollection;
|
||||
|
||||
class Packages extends CachedCollection
|
||||
{
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$items = [
|
||||
'plugins' => new Plugins($refresh, $callback),
|
||||
'themes' => new Themes($refresh, $callback)
|
||||
];
|
||||
|
||||
parent::__construct($items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
/**
|
||||
* Class Plugins
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Plugins extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'plugins';
|
||||
|
||||
protected $repository = 'https://getgrav.org/downloads/plugins.json';
|
||||
|
||||
/**
|
||||
* Local Plugins Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
parent::__construct($this->repository, $refresh, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM\Remote;
|
||||
|
||||
/**
|
||||
* Class Themes
|
||||
* @package Grav\Common\GPM\Remote
|
||||
*/
|
||||
class Themes extends AbstractPackageCollection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'themes';
|
||||
|
||||
protected $repository = 'https://getgrav.org/downloads/themes.json';
|
||||
|
||||
/**
|
||||
* Local Themes Constructor
|
||||
* @param bool $refresh
|
||||
* @param callable $callback Either a function or callback in array notation
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
parent::__construct($this->repository, $refresh, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\GravTrait;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* 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_TIMEOUT => 15,
|
||||
CURLOPT_HEADER => false,
|
||||
/**
|
||||
* 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,
|
||||
/**
|
||||
* 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) {
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(self::$defaults, $options);
|
||||
$method = 'get' . ucfirst(strtolower(self::$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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress normalized for cURL and Fopen
|
||||
* Accepts a vsariable length of arguments passed in by stream method
|
||||
*
|
||||
* @return array Normalized array with useful data.
|
||||
* Format: ['code' => int|false, 'filesize' => bytes, 'transferred' => bytes, 'percent' => int]
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 proxy set add that
|
||||
$proxy_url = self::getGrav()['config']->get('system.proxy_url');
|
||||
if ($proxy_url) {
|
||||
$options['fopen']['proxy'] = $proxy_url;
|
||||
}
|
||||
|
||||
if ($callback) {
|
||||
$options['fopen']['notification'] = ['self', 'progress'];
|
||||
}
|
||||
|
||||
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
|
||||
$content = @file_get_contents($uri, false, $stream);
|
||||
|
||||
if ($content === false) {
|
||||
throw new \RuntimeException("Error while trying to download '$uri'");
|
||||
}
|
||||
|
||||
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) {
|
||||
$error_message = curl_strerror($errno);
|
||||
throw new \RuntimeException("cURL error ({$errno}):\n {$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']
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// if proxy set add that
|
||||
$proxy_url = self::getGrav()['config']->get('system.proxy_url');
|
||||
if ($proxy_url) {
|
||||
$options['curl'][CURLOPT_PROXY] = $proxy_url;
|
||||
}
|
||||
|
||||
// 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] : 3;
|
||||
$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) {
|
||||
preg_match('/Location:(.*?)\n/', $header, $matches);
|
||||
$uri = trim(array_pop($matches));
|
||||
} else {
|
||||
$code = 0;
|
||||
}
|
||||
}
|
||||
} while ($code && --$max_redirects);
|
||||
|
||||
curl_close($rch);
|
||||
|
||||
if (!$max_redirects) {
|
||||
if ($max_redirects === null) {
|
||||
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $uri);
|
||||
|
||||
return curl_exec($ch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Grav\Common\GPM;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Class Upgrader
|
||||
*
|
||||
* @package Grav\Common\GPM
|
||||
*/
|
||||
class Upgrader
|
||||
{
|
||||
/**
|
||||
* Remote details about latest Grav version
|
||||
*
|
||||
* @var Packages
|
||||
*/
|
||||
private $remote;
|
||||
|
||||
/**
|
||||
* Internal cache
|
||||
*
|
||||
* @var Iterator
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __construct($refresh = false, $callback = null)
|
||||
{
|
||||
$this->remote = new Remote\Grav($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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function meetsRequirements()
|
||||
{
|
||||
if (version_compare(PHP_VERSION, GRAV_PHP_MIN, '<')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the currently installed Grav is upgradable to a newer version
|
||||
*
|
||||
* @return boolean True if it's upgradable, False otherwise.
|
||||
*/
|
||||
public function isUpgradable()
|
||||
{
|
||||
return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Grav is currently symbolically linked
|
||||
*
|
||||
* @return boolean True if Grav is symlinked, False otherwise.
|
||||
*/
|
||||
|
||||
public function isSymlink()
|
||||
{
|
||||
return $this->remote->isSymlink();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Abstract class to implement magic __get(), __set(), __isset() and __unset().
|
||||
* Also implements ArrayAccess.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Medium\ImageMedium;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Service\ConfigServiceProvider;
|
||||
use Grav\Common\Service\ErrorServiceProvider;
|
||||
use Grav\Common\Service\LoggerServiceProvider;
|
||||
use Grav\Common\Service\StreamsServiceProvider;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use RocketTheme\Toolbox\DI\Container;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\Event\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Grav
|
||||
*
|
||||
* @author Andy Miller
|
||||
* @link http://www.rockettheme.com
|
||||
* @license http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Influenced by Pico, Stacey, Kirby, PieCrust and other great platforms...
|
||||
*/
|
||||
class Grav extends Container
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $output;
|
||||
|
||||
/**
|
||||
* @var static
|
||||
*/
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
GravTrait::setGrav(self::$instance);
|
||||
|
||||
} elseif ($values) {
|
||||
$instance = self::$instance;
|
||||
foreach ($values as $key => $value) {
|
||||
$instance->offsetSet($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$container['debugger']->startTimer('_services', 'Services');
|
||||
|
||||
$container->register(new LoggerServiceProvider);
|
||||
|
||||
$container->register(new ErrorServiceProvider);
|
||||
|
||||
$container['uri'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Uri($c);
|
||||
};
|
||||
|
||||
$container['task'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return !empty($_POST['task']) ? $_POST['task'] : $c['uri']->param('task');
|
||||
};
|
||||
|
||||
$container['events'] = function () {
|
||||
return new EventDispatcher;
|
||||
};
|
||||
$container['cache'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Cache($c);
|
||||
};
|
||||
$container['session'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Session($c);
|
||||
};
|
||||
$container['plugins'] = function () {
|
||||
return new Plugins();
|
||||
};
|
||||
$container['themes'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Themes($c);
|
||||
};
|
||||
$container['twig'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Twig($c);
|
||||
};
|
||||
$container['taxonomy'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Taxonomy($c);
|
||||
};
|
||||
$container['language'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Language($c);
|
||||
};
|
||||
|
||||
$container['pages'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return new Pages($c);
|
||||
};
|
||||
|
||||
$container['assets'] = new Assets();
|
||||
|
||||
$container['page'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $c['pages'];
|
||||
/** @var Language $language */
|
||||
$language = $c['language'];
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = $c['uri'];
|
||||
|
||||
$path = $uri->path(); // Don't trim to support trailing slash default routes
|
||||
$path = $path ?: '/';
|
||||
|
||||
$page = $pages->dispatch($path);
|
||||
|
||||
// Redirection tests
|
||||
if ($page) {
|
||||
// Language-specific redirection scenarios
|
||||
if ($language->enabled()) {
|
||||
if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) {
|
||||
$c->redirect($page->route());
|
||||
}
|
||||
if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) {
|
||||
$c->redirectLangSafe($page->route());
|
||||
}
|
||||
}
|
||||
// Default route test and redirect
|
||||
if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) {
|
||||
$c->redirectLangSafe($page->route());
|
||||
}
|
||||
}
|
||||
|
||||
// if page is not found, try some fallback stuff
|
||||
if (!$page || !$page->routable()) {
|
||||
|
||||
// Try fallback URL stuff...
|
||||
$c->fallbackUrl($path);
|
||||
|
||||
// If no page found, fire event
|
||||
$event = $c->fireEvent('onPageNotFound');
|
||||
|
||||
if (isset($event->page)) {
|
||||
$page = $event->page;
|
||||
} else {
|
||||
throw new \RuntimeException('Page Not Found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
return $page;
|
||||
};
|
||||
|
||||
$container['output'] = function ($c) {
|
||||
/** @var Grav $c */
|
||||
return $c['twig']->processSite($c['uri']->extension());
|
||||
};
|
||||
|
||||
$container['browser'] = function () {
|
||||
return new Browser();
|
||||
};
|
||||
|
||||
$container->register(new StreamsServiceProvider);
|
||||
$container->register(new ConfigServiceProvider);
|
||||
|
||||
$container['inflector'] = new Inflector();
|
||||
|
||||
$container['debugger']->stopTimer('_services');
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this['debugger'];
|
||||
|
||||
// Load site setup and initializing streams.
|
||||
$debugger->startTimer('_setup', 'Site Setup');
|
||||
$this['setup']->init();
|
||||
$this['streams'];
|
||||
$debugger->stopTimer('_setup');
|
||||
|
||||
// Initialize configuration.
|
||||
$debugger->startTimer('_config', 'Configuration');
|
||||
$this['config']->init();
|
||||
$debugger->stopTimer('_config');
|
||||
|
||||
// Initialize error handlers.
|
||||
$this['errors']->resetHandlers();
|
||||
|
||||
// Initialize debugger.
|
||||
$debugger->init();
|
||||
$debugger->startTimer('init', 'Initialize');
|
||||
$this['config']->debug();
|
||||
|
||||
// Use output buffering to prevent headers from being sent too early.
|
||||
ob_start();
|
||||
if ($this['config']->get('system.cache.gzip')) {
|
||||
// Enable zip/deflate with a fallback in case of if browser does not support compressing.
|
||||
if (!ob_start("ob_gzhandler")) {
|
||||
ob_start();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the timezone.
|
||||
if ($this['config']->get('system.timezone')) {
|
||||
date_default_timezone_set($this['config']->get('system.timezone'));
|
||||
}
|
||||
|
||||
// Initialize uri, session.
|
||||
$this['uri']->init();
|
||||
$this['session']->init();
|
||||
|
||||
// Initialize Locale if set and configured.
|
||||
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
|
||||
setlocale(LC_ALL, $this['language']->getLanguage());
|
||||
} elseif ($this['config']->get('system.default_locale')) {
|
||||
setlocale(LC_ALL, $this['config']->get('system.default_locale'));
|
||||
}
|
||||
|
||||
$debugger->stopTimer('init');
|
||||
|
||||
$debugger->startTimer('plugins', 'Plugins');
|
||||
$this['plugins']->init();
|
||||
$this->fireEvent('onPluginsInitialized');
|
||||
$debugger->stopTimer('plugins');
|
||||
|
||||
$debugger->startTimer('themes', 'Themes');
|
||||
$this['themes']->init();
|
||||
$debugger->stopTimer('themes');
|
||||
|
||||
$task = $this['task'];
|
||||
if ($task) {
|
||||
$this->fireEvent('onTask.' . $task);
|
||||
}
|
||||
|
||||
$this['assets']->init();
|
||||
$this->fireEvent('onAssetsInitialized');
|
||||
|
||||
$debugger->startTimer('twig', 'Twig');
|
||||
$this['twig']->init();
|
||||
$debugger->stopTimer('twig');
|
||||
|
||||
$debugger->startTimer('pages', 'Pages');
|
||||
$this['pages']->init();
|
||||
$this->fireEvent('onPagesInitialized');
|
||||
$debugger->stopTimer('pages');
|
||||
$this->fireEvent('onPageInitialized');
|
||||
|
||||
$debugger->addAssets();
|
||||
|
||||
// Process whole page as required
|
||||
$debugger->startTimer('render', 'Render');
|
||||
$this->output = $this['output'];
|
||||
$this->fireEvent('onOutputGenerated');
|
||||
$debugger->stopTimer('render');
|
||||
|
||||
// Set the header type
|
||||
$this->header();
|
||||
echo $this->output;
|
||||
$debugger->render();
|
||||
|
||||
$this->fireEvent('onOutputRendered');
|
||||
|
||||
register_shutdown_function([$this, 'shutdown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', 301);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
/** @var Language $language */
|
||||
$language = $this['language'];
|
||||
|
||||
if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) {
|
||||
$this->redirect($language->getLanguage() . $route, $code);
|
||||
} else {
|
||||
$this->redirect($route, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mime type for the file format.
|
||||
*
|
||||
* @param string $format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mime($format)
|
||||
{
|
||||
switch ($format) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'html':
|
||||
return 'text/html';
|
||||
case 'atom':
|
||||
return 'application/atom+xml';
|
||||
case 'rss':
|
||||
return 'application/rss+xml';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return 'text/html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response header.
|
||||
*/
|
||||
public function header()
|
||||
{
|
||||
$extension = $this['uri']->extension();
|
||||
|
||||
/** @var Page $page */
|
||||
$page = $this['page'];
|
||||
|
||||
header('Content-type: ' . $this->mime($extension));
|
||||
|
||||
// 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';
|
||||
header('Cache-Control: max-age=' . $expires);
|
||||
header('Expires: ' . $expires_date);
|
||||
}
|
||||
|
||||
// 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 debugger data in headers
|
||||
if (!($extension === null || $extension == 'html')) {
|
||||
$this['debugger']->enabled(false);
|
||||
}
|
||||
|
||||
// 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 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.
|
||||
header('Content-Encoding: none');
|
||||
}
|
||||
|
||||
// Get length and close the connection.
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
header("Connection: close");
|
||||
|
||||
// Finally flush the regular buffer.
|
||||
ob_end_flush();
|
||||
@ob_flush();
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Run any time consuming tasks.
|
||||
$this->fireEvent('onShutdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* This attempts to find media, other files, and download them
|
||||
*
|
||||
* @param $path
|
||||
*/
|
||||
protected function fallbackUrl($path)
|
||||
{
|
||||
/** @var Uri $uri */
|
||||
$uri = $this['uri'];
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $this['config'];
|
||||
|
||||
$uri_extension = $uri->extension();
|
||||
$fallback_types = $config->get('system.media.allowed_fallback_types', null);
|
||||
$supported_types = $config->get('media');
|
||||
|
||||
// Check whitelist first, then ensure extension is a valid media type
|
||||
if (!empty($fallback_types) && !in_array($uri_extension, $fallback_types)) {
|
||||
return;
|
||||
} elseif (!array_key_exists($uri_extension, $supported_types)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Class GravTrait
|
||||
*
|
||||
* @package Grav\Common
|
||||
*/
|
||||
trait GravTrait
|
||||
{
|
||||
/**
|
||||
* @var Grav
|
||||
*/
|
||||
protected static $grav;
|
||||
|
||||
/**
|
||||
* @return Grav
|
||||
*/
|
||||
public static function getGrav()
|
||||
{
|
||||
if (!self::$grav) {
|
||||
self::$grav = Grav::instance();
|
||||
}
|
||||
|
||||
return self::$grav;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public static function setGrav(Grav $grav)
|
||||
{
|
||||
self::$grav = $grav;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
namespace Grav\Common\Helpers;
|
||||
|
||||
use DOMDocument;
|
||||
|
||||
/**
|
||||
* This file is part of urodoz/truncateHTML.
|
||||
*
|
||||
* (c) Albert Lacarta <urodoz@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class Truncator {
|
||||
|
||||
public static $default_options = array(
|
||||
'ellipsis' => '…',
|
||||
'break' => ' ',
|
||||
'length_in_chars' => false,
|
||||
'word_safe' => false,
|
||||
);
|
||||
|
||||
// These tags are allowed to have an ellipsis inside
|
||||
public static $ellipsable_tags = array(
|
||||
'p', 'ol', 'ul', 'li',
|
||||
'div', 'header', 'article', 'nav',
|
||||
'section', 'footer', 'aside',
|
||||
'dd', 'dt', 'dl',
|
||||
);
|
||||
|
||||
public static $self_closing_tags = array(
|
||||
'br', 'hr', 'img',
|
||||
);
|
||||
|
||||
/**
|
||||
* Truncate given HTML string to specified length.
|
||||
* If length_in_chars is false it's trimmed by number
|
||||
* of words, otherwise by number of characters.
|
||||
*
|
||||
* @param string $html
|
||||
* @param integer $length
|
||||
* @param string|array $opts
|
||||
* @return string
|
||||
*/
|
||||
public static function truncate($html, $length, $opts=array())
|
||||
{
|
||||
if (is_string($opts)) $opts = array('ellipsis' => $opts);
|
||||
$opts = array_merge(static::$default_options, $opts);
|
||||
// wrap the html in case it consists of adjacent nodes like <p>foo</p><p>bar</p>
|
||||
$html = mb_convert_encoding("<div>".$html."</div>", 'HTML-ENTITIES', 'UTF-8');
|
||||
|
||||
$root_node = null;
|
||||
// Parse using HTML5Lib if it's available.
|
||||
if (class_exists('HTML5Lib\\Parser')) {
|
||||
try {
|
||||
$doc = \HTML5Lib\Parser::parse($html);
|
||||
$root_node = $doc->documentElement->lastChild->lastChild;
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
;
|
||||
}
|
||||
}
|
||||
if ($root_node === null) {
|
||||
// HTML5Lib not available so we'll have to use DOMDocument
|
||||
// We'll only be able to parse HTML5 if it's valid XML
|
||||
$doc = new DOMDocument('4.01', 'utf-8');
|
||||
$doc->formatOutput = false;
|
||||
$doc->preserveWhiteSpace = true;
|
||||
// loadHTML will fail with HTML5 tags (article, nav, etc)
|
||||
// so we need to suppress errors and if it fails to parse we
|
||||
// retry with the XML parser instead
|
||||
$prev_use_errors = libxml_use_internal_errors(true);
|
||||
if ($doc->loadHTML($html)) {
|
||||
$root_node = $doc->documentElement->lastChild->lastChild;
|
||||
}
|
||||
else if ($doc->loadXML($html)) {
|
||||
$root_node = $doc->documentElement;
|
||||
}
|
||||
else {
|
||||
libxml_use_internal_errors($prev_use_errors);
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
libxml_use_internal_errors($prev_use_errors);
|
||||
}
|
||||
list($text, $_, $opts) = static::truncateNode($doc, $root_node, $length, $opts);
|
||||
|
||||
$text = mb_substr(mb_substr($text, 0, -6), 5);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
protected static function truncateNode($doc, $node, $length, $opts)
|
||||
{
|
||||
if ($length === 0 && !static::ellipsable($node)) {
|
||||
return array('', 1, $opts);
|
||||
}
|
||||
list($inner, $remaining, $opts) = static::innerTruncate($doc, $node, $length, $opts);
|
||||
if (0 === mb_strlen($inner)) {
|
||||
return array(in_array(mb_strtolower($node->nodeName), static::$self_closing_tags) ? $doc->saveXML($node) : "", $length - $remaining, $opts);
|
||||
}
|
||||
while($node->firstChild) {
|
||||
$node->removeChild($node->firstChild);
|
||||
}
|
||||
$newNode = $doc->createDocumentFragment();
|
||||
// handle the ampersand
|
||||
$newNode->appendXml(static::xmlEscape($inner));
|
||||
$node->appendChild($newNode);
|
||||
return array($doc->saveXML($node), $length - $remaining, $opts);
|
||||
}
|
||||
|
||||
protected static function innerTruncate($doc, $node, $length, $opts)
|
||||
{
|
||||
$inner = '';
|
||||
$remaining = $length;
|
||||
foreach($node->childNodes as $childNode) {
|
||||
if ($childNode->nodeType === XML_ELEMENT_NODE) {
|
||||
list($txt, $nb, $opts) = static::truncateNode($doc, $childNode, $remaining, $opts);
|
||||
}
|
||||
else if ($childNode->nodeType === XML_TEXT_NODE) {
|
||||
list($txt, $nb, $opts) = static::truncateText($childNode, $remaining, $opts);
|
||||
} else {
|
||||
$txt = '';
|
||||
$nb = 0;
|
||||
}
|
||||
|
||||
// unhandle the ampersand
|
||||
$txt = static::xmlUnescape($txt);
|
||||
|
||||
$remaining -= $nb;
|
||||
$inner .= $txt;
|
||||
if ($remaining < 0) {
|
||||
if (static::ellipsable($node)) {
|
||||
$inner = preg_replace('/(?:[\s\pP]+|(?:&(?:[a-z]+|#[0-9]+);?))*$/u', '', $inner).$opts['ellipsis'];
|
||||
$opts['ellipsis'] = '';
|
||||
$opts['was_truncated'] = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return array($inner, $remaining, $opts);
|
||||
}
|
||||
|
||||
protected static function truncateText($node, $length, $opts)
|
||||
{
|
||||
$string = $node->textContent;
|
||||
|
||||
if ($opts['length_in_chars']) {
|
||||
$count = mb_strlen($string);
|
||||
if ($count <= $length && $length > 0) {
|
||||
return array($string, $count, $opts);
|
||||
}
|
||||
if ($opts['word_safe']) {
|
||||
if (false !== ($breakpoint = mb_strpos($string, $opts['break'], $length))) {
|
||||
if ($breakpoint < mb_strlen($string) - 1) {
|
||||
$string = mb_substr($string, 0, $breakpoint) . $opts['break'];
|
||||
}
|
||||
}
|
||||
return array($string, $count, $opts);
|
||||
}
|
||||
return array(mb_substr($node->textContent, 0, $length), $count, $opts);
|
||||
}
|
||||
else {
|
||||
preg_match_all('/\s*\S+/', $string, $words);
|
||||
$words = $words[0];
|
||||
$count = count($words);
|
||||
if ($count <= $length && $length > 0) {
|
||||
return array($string, $count, $opts);
|
||||
}
|
||||
return array(implode('', array_slice($words, 0, $length)), $count, $opts);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function ellipsable($node)
|
||||
{
|
||||
return ($node instanceof DOMDocument)
|
||||
|| in_array(mb_strtolower($node->nodeName), static::$ellipsable_tags)
|
||||
;
|
||||
}
|
||||
|
||||
protected static function xmlEscape($string)
|
||||
{
|
||||
$string = str_replace('&', '&', $string);
|
||||
$string = str_replace('<?', '<?', $string);
|
||||
return $string;
|
||||
}
|
||||
|
||||
protected static function xmlUnescape($string)
|
||||
{
|
||||
$string = str_replace('&', '&', $string);
|
||||
$string = str_replace('<?', '<?', $string);
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* This file was originally part of the Akelos Framework
|
||||
*/
|
||||
|
||||
/**
|
||||
* Inflector for pluralize and singularize English nouns.
|
||||
*
|
||||
* This Inflector is a port of Ruby on Rails Inflector.
|
||||
*
|
||||
* It can be really helpful for developers that want to
|
||||
* create frameworks based on naming conventions rather than
|
||||
* configurations.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
class Inflector
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected $plural;
|
||||
protected $singular;
|
||||
protected $uncountable;
|
||||
protected $irregular;
|
||||
protected $ordinals;
|
||||
|
||||
public function init()
|
||||
{
|
||||
if (empty($this->plural)) {
|
||||
$language = self::getGrav()['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-zd])([A-Z])/', '\1-\2', $regex1);
|
||||
$regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex2);
|
||||
|
||||
return strtolower($regex3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
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
|
||||
* @package Grav\Common
|
||||
*/
|
||||
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)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
namespace Grav\Common\Language;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Language and translation functionality for Grav
|
||||
*/
|
||||
class Language
|
||||
{
|
||||
protected $grav;
|
||||
protected $enabled = true;
|
||||
protected $languages = [];
|
||||
protected $page_extensions = [];
|
||||
protected $fallback_languages = [];
|
||||
protected $default;
|
||||
protected $active = null;
|
||||
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 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] . "/", '', $matches[0], 1);
|
||||
|
||||
// store in session if different
|
||||
if ($this->config->get('system.session.enabled', false)
|
||||
&& $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 from session, else no active
|
||||
if ($this->config->get('system.session.enabled', false) &&
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)$this->config->getLanguages()->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 = $this->config->getLanguages()->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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
<?php
|
||||
namespace Grav\Common\Language;
|
||||
|
||||
/**
|
||||
* Language and translation functionality for Grav
|
||||
*/
|
||||
class LanguageCodes
|
||||
{
|
||||
protected static $codes = [
|
||||
"ab" => [
|
||||
"name" => "Abkhaz",
|
||||
"nativeName" => "аҧсуа"
|
||||
],
|
||||
"aa" => [
|
||||
"name" => "Afar",
|
||||
"nativeName" => "Afaraf"
|
||||
],
|
||||
"af" => [
|
||||
"name" => "Afrikaans",
|
||||
"nativeName" => "Afrikaans"
|
||||
],
|
||||
"ak" => [
|
||||
"name" => "Akan",
|
||||
"nativeName" => "Akan"
|
||||
],
|
||||
"sq" => [
|
||||
"name" => "Albanian",
|
||||
"nativeName" => "Shqip"
|
||||
],
|
||||
"am" => [
|
||||
"name" => "Amharic",
|
||||
"nativeName" => "አማርኛ"
|
||||
],
|
||||
"ar" => [
|
||||
"name" => "Arabic",
|
||||
"nativeName" => "العربية"
|
||||
],
|
||||
"an" => [
|
||||
"name" => "Aragonese",
|
||||
"nativeName" => "Aragonés"
|
||||
],
|
||||
"hy" => [
|
||||
"name" => "Armenian",
|
||||
"nativeName" => "Հայերեն"
|
||||
],
|
||||
"as" => [
|
||||
"name" => "Assamese",
|
||||
"nativeName" => "অসমীয়া"
|
||||
],
|
||||
"av" => [
|
||||
"name" => "Avaric",
|
||||
"nativeName" => "авар мацӀ"
|
||||
],
|
||||
"ae" => [
|
||||
"name" => "Avestan",
|
||||
"nativeName" => "avesta"
|
||||
],
|
||||
"ay" => [
|
||||
"name" => "Aymara",
|
||||
"nativeName" => "aymar aru"
|
||||
],
|
||||
"az" => [
|
||||
"name" => "Azerbaijani",
|
||||
"nativeName" => "azərbaycan dili"
|
||||
],
|
||||
"bm" => [
|
||||
"name" => "Bambara",
|
||||
"nativeName" => "bamanankan"
|
||||
],
|
||||
"ba" => [
|
||||
"name" => "Bashkir",
|
||||
"nativeName" => "башҡорт теле"
|
||||
],
|
||||
"eu" => [
|
||||
"name" => "Basque",
|
||||
"nativeName" => "euskara"
|
||||
],
|
||||
"be" => [
|
||||
"name" => "Belarusian",
|
||||
"nativeName" => "Беларуская"
|
||||
],
|
||||
"bn" => [
|
||||
"name" => "Bengali",
|
||||
"nativeName" => "বাংলা"
|
||||
],
|
||||
"bh" => [
|
||||
"name" => "Bihari",
|
||||
"nativeName" => "भोजपुरी"
|
||||
],
|
||||
"bi" => [
|
||||
"name" => "Bislama",
|
||||
"nativeName" => "Bislama"
|
||||
],
|
||||
"bs" => [
|
||||
"name" => "Bosnian",
|
||||
"nativeName" => "bosanski jezik"
|
||||
],
|
||||
"br" => [
|
||||
"name" => "Breton",
|
||||
"nativeName" => "brezhoneg"
|
||||
],
|
||||
"bg" => [
|
||||
"name" => "Bulgarian",
|
||||
"nativeName" => "български език"
|
||||
],
|
||||
"my" => [
|
||||
"name" => "Burmese",
|
||||
"nativeName" => "ဗမာစာ"
|
||||
],
|
||||
"ca" => [
|
||||
"name" => "Catalan",
|
||||
"nativeName" => "Català"
|
||||
],
|
||||
"ch" => [
|
||||
"name" => "Chamorro",
|
||||
"nativeName" => "Chamoru"
|
||||
],
|
||||
"ce" => [
|
||||
"name" => "Chechen",
|
||||
"nativeName" => "нохчийн мотт"
|
||||
],
|
||||
"ny" => [
|
||||
"name" => "Chichewa",
|
||||
"nativeName" => "chiCheŵa"
|
||||
],
|
||||
"zh" => [
|
||||
"name" => "Chinese",
|
||||
"nativeName" => "中文"
|
||||
],
|
||||
"cv" => [
|
||||
"name" => "Chuvash",
|
||||
"nativeName" => "чӑваш чӗлхи"
|
||||
],
|
||||
"kw" => [
|
||||
"name" => "Cornish",
|
||||
"nativeName" => "Kernewek"
|
||||
],
|
||||
"co" => [
|
||||
"name" => "Corsican",
|
||||
"nativeName" => "corsu"
|
||||
],
|
||||
"cr" => [
|
||||
"name" => "Cree",
|
||||
"nativeName" => "ᓀᐦᐃᔭᐍᐏᐣ"
|
||||
],
|
||||
"hr" => [
|
||||
"name" => "Croatian",
|
||||
"nativeName" => "hrvatski"
|
||||
],
|
||||
"cs" => [
|
||||
"name" => "Czech",
|
||||
"nativeName" => "česky"
|
||||
],
|
||||
"da" => [
|
||||
"name" => "Danish",
|
||||
"nativeName" => "dansk"
|
||||
],
|
||||
"dv" => [
|
||||
"name" => "Divehi",
|
||||
"nativeName" => "ދިވެހި"
|
||||
],
|
||||
"nl" => [
|
||||
"name" => "Dutch",
|
||||
"nativeName" => "Nederlands"
|
||||
],
|
||||
"en" => [
|
||||
"name" => "English",
|
||||
"nativeName" => "English"
|
||||
],
|
||||
"eo" => [
|
||||
"name" => "Esperanto",
|
||||
"nativeName" => "Esperanto"
|
||||
],
|
||||
"et" => [
|
||||
"name" => "Estonian",
|
||||
"nativeName" => "eesti"
|
||||
],
|
||||
"ee" => [
|
||||
"name" => "Ewe",
|
||||
"nativeName" => "Eʋegbe"
|
||||
],
|
||||
"fo" => [
|
||||
"name" => "Faroese",
|
||||
"nativeName" => "føroyskt"
|
||||
],
|
||||
"fj" => [
|
||||
"name" => "Fijian",
|
||||
"nativeName" => "vosa Vakaviti"
|
||||
],
|
||||
"fi" => [
|
||||
"name" => "Finnish",
|
||||
"nativeName" => "suomi"
|
||||
],
|
||||
"fr" => [
|
||||
"name" => "French",
|
||||
"nativeName" => "Français"
|
||||
],
|
||||
"ff" => [
|
||||
"name" => "Fula",
|
||||
"nativeName" => "Fulfulde"
|
||||
],
|
||||
"gl" => [
|
||||
"name" => "Galician",
|
||||
"nativeName" => "Galego"
|
||||
],
|
||||
"ka" => [
|
||||
"name" => "Georgian",
|
||||
"nativeName" => "ქართული"
|
||||
],
|
||||
"de" => [
|
||||
"name" => "German",
|
||||
"nativeName" => "Deutsch"
|
||||
],
|
||||
"el" => [
|
||||
"name" => "Greek",
|
||||
"nativeName" => "Ελληνικά"
|
||||
],
|
||||
"gn" => [
|
||||
"name" => "Guaraní",
|
||||
"nativeName" => "Avañeẽ"
|
||||
],
|
||||
"gu" => [
|
||||
"name" => "Gujarati",
|
||||
"nativeName" => "ગુજરાતી"
|
||||
],
|
||||
"ht" => [
|
||||
"name" => "Haitian",
|
||||
"nativeName" => "Kreyòl ayisyen"
|
||||
],
|
||||
"ha" => [
|
||||
"name" => "Hausa",
|
||||
"nativeName" => "هَوُسَ"
|
||||
],
|
||||
"he" => [
|
||||
"name" => "Hebrew",
|
||||
"nativeName" => "עברית"
|
||||
],
|
||||
"hz" => [
|
||||
"name" => "Herero",
|
||||
"nativeName" => "Otjiherero"
|
||||
],
|
||||
"hi" => [
|
||||
"name" => "Hindi",
|
||||
"nativeName" => "हिन्दी"
|
||||
],
|
||||
"ho" => [
|
||||
"name" => "Hiri Motu",
|
||||
"nativeName" => "Hiri Motu"
|
||||
],
|
||||
"hu" => [
|
||||
"name" => "Hungarian",
|
||||
"nativeName" => "Magyar"
|
||||
],
|
||||
"ia" => [
|
||||
"name" => "Interlingua",
|
||||
"nativeName" => "Interlingua"
|
||||
],
|
||||
"id" => [
|
||||
"name" => "Indonesian",
|
||||
"nativeName" => "Bahasa Indonesia"
|
||||
],
|
||||
"ie" => [
|
||||
"name" => "Interlingue",
|
||||
"nativeName" => "Interlingue"
|
||||
],
|
||||
"ga" => [
|
||||
"name" => "Irish",
|
||||
"nativeName" => "Gaeilge"
|
||||
],
|
||||
"ig" => [
|
||||
"name" => "Igbo",
|
||||
"nativeName" => "Asụsụ Igbo"
|
||||
],
|
||||
"ik" => [
|
||||
"name" => "Inupiaq",
|
||||
"nativeName" => "Iñupiaq"
|
||||
],
|
||||
"io" => [
|
||||
"name" => "Ido",
|
||||
"nativeName" => "Ido"
|
||||
],
|
||||
"is" => [
|
||||
"name" => "Icelandic",
|
||||
"nativeName" => "Íslenska"
|
||||
],
|
||||
"it" => [
|
||||
"name" => "Italian",
|
||||
"nativeName" => "Italiano"
|
||||
],
|
||||
"iu" => [
|
||||
"name" => "Inuktitut",
|
||||
"nativeName" => "ᐃᓄᒃᑎᑐᑦ"
|
||||
],
|
||||
"ja" => [
|
||||
"name" => "Japanese",
|
||||
"nativeName" => "日本語"
|
||||
],
|
||||
"jv" => [
|
||||
"name" => "Javanese",
|
||||
"nativeName" => "basa Jawa"
|
||||
],
|
||||
"kl" => [
|
||||
"name" => "Kalaallisut",
|
||||
"nativeName" => "kalaallisut"
|
||||
],
|
||||
"kn" => [
|
||||
"name" => "Kannada",
|
||||
"nativeName" => "ಕನ್ನಡ"
|
||||
],
|
||||
"kr" => [
|
||||
"name" => "Kanuri",
|
||||
"nativeName" => "Kanuri"
|
||||
],
|
||||
"ks" => [
|
||||
"name" => "Kashmiri",
|
||||
"nativeName" => "कश्मीरी"
|
||||
],
|
||||
"kk" => [
|
||||
"name" => "Kazakh",
|
||||
"nativeName" => "Қазақ тілі"
|
||||
],
|
||||
"km" => [
|
||||
"name" => "Khmer",
|
||||
"nativeName" => "ភាសាខ្មែរ"
|
||||
],
|
||||
"ki" => [
|
||||
"name" => "Kikuyu",
|
||||
"nativeName" => "Gĩkũyũ"
|
||||
],
|
||||
"rw" => [
|
||||
"name" => "Kinyarwanda",
|
||||
"nativeName" => "Ikinyarwanda"
|
||||
],
|
||||
"ky" => [
|
||||
"name" => "Kirghiz",
|
||||
"nativeName" => "кыргыз тили"
|
||||
],
|
||||
"kv" => [
|
||||
"name" => "Komi",
|
||||
"nativeName" => "коми кыв"
|
||||
],
|
||||
"kg" => [
|
||||
"name" => "Kongo",
|
||||
"nativeName" => "KiKongo"
|
||||
],
|
||||
"ko" => [
|
||||
"name" => "Korean",
|
||||
"nativeName" => "한국어"
|
||||
],
|
||||
"ku" => [
|
||||
"name" => "Kurdish",
|
||||
"nativeName" => "كوردی"
|
||||
],
|
||||
"kj" => [
|
||||
"name" => "Kwanyama",
|
||||
"nativeName" => "Kuanyama"
|
||||
],
|
||||
"la" => [
|
||||
"name" => "Latin",
|
||||
"nativeName" => "latine"
|
||||
],
|
||||
"lb" => [
|
||||
"name" => "Luxembourgish",
|
||||
"nativeName" => "Lëtzebuergesch"
|
||||
],
|
||||
"lg" => [
|
||||
"name" => "Luganda",
|
||||
"nativeName" => "Luganda"
|
||||
],
|
||||
"li" => [
|
||||
"name" => "Limburgish",
|
||||
"nativeName" => "Limburgs"
|
||||
],
|
||||
"ln" => [
|
||||
"name" => "Lingala",
|
||||
"nativeName" => "Lingála"
|
||||
],
|
||||
"lo" => [
|
||||
"name" => "Lao",
|
||||
"nativeName" => "ພາສາລາວ"
|
||||
],
|
||||
"lt" => [
|
||||
"name" => "Lithuanian",
|
||||
"nativeName" => "lietuvių kalba"
|
||||
],
|
||||
"lu" => [
|
||||
"name" => "Luba-Katanga",
|
||||
"nativeName" => "Luba-Katanga"
|
||||
],
|
||||
"lv" => [
|
||||
"name" => "Latvian",
|
||||
"nativeName" => "latviešu valoda"
|
||||
],
|
||||
"gv" => [
|
||||
"name" => "Manx",
|
||||
"nativeName" => "Gaelg"
|
||||
],
|
||||
"mk" => [
|
||||
"name" => "Macedonian",
|
||||
"nativeName" => "македонски јазик"
|
||||
],
|
||||
"mg" => [
|
||||
"name" => "Malagasy",
|
||||
"nativeName" => "Malagasy fiteny"
|
||||
],
|
||||
"ms" => [
|
||||
"name" => "Malay",
|
||||
"nativeName" => "بهاس ملايو"
|
||||
],
|
||||
"ml" => [
|
||||
"name" => "Malayalam",
|
||||
"nativeName" => "മലയാളം"
|
||||
],
|
||||
"mt" => [
|
||||
"name" => "Maltese",
|
||||
"nativeName" => "Malti"
|
||||
],
|
||||
"mi" => [
|
||||
"name" => "Māori",
|
||||
"nativeName" => "te reo Māori"
|
||||
],
|
||||
"mr" => [
|
||||
"name" => "Marathi",
|
||||
"nativeName" => "मराठी"
|
||||
],
|
||||
"mh" => [
|
||||
"name" => "Marshallese",
|
||||
"nativeName" => "Kajin M̧ajeļ"
|
||||
],
|
||||
"mn" => [
|
||||
"name" => "Mongolian",
|
||||
"nativeName" => "монгол"
|
||||
],
|
||||
"na" => [
|
||||
"name" => "Nauru",
|
||||
"nativeName" => "Ekakairũ Naoero"
|
||||
],
|
||||
"nv" => [
|
||||
"name" => "Navajo",
|
||||
"nativeName" => "Diné bizaad"
|
||||
],
|
||||
"nb" => [
|
||||
"name" => "Norwegian Bokmål",
|
||||
"nativeName" => "Norsk bokmål"
|
||||
],
|
||||
"nd" => [
|
||||
"name" => "North Ndebele",
|
||||
"nativeName" => "isiNdebele"
|
||||
],
|
||||
"ne" => [
|
||||
"name" => "Nepali",
|
||||
"nativeName" => "नेपाली"
|
||||
],
|
||||
"ng" => [
|
||||
"name" => "Ndonga",
|
||||
"nativeName" => "Owambo"
|
||||
],
|
||||
"nn" => [
|
||||
"name" => "Norwegian Nynorsk",
|
||||
"nativeName" => "Norsk nynorsk"
|
||||
],
|
||||
"no" => [
|
||||
"name" => "Norwegian",
|
||||
"nativeName" => "Norsk"
|
||||
],
|
||||
"ii" => [
|
||||
"name" => "Nuosu",
|
||||
"nativeName" => "ꆈꌠ꒿ Nuosuhxop"
|
||||
],
|
||||
"nr" => [
|
||||
"name" => "South Ndebele",
|
||||
"nativeName" => "isiNdebele"
|
||||
],
|
||||
"oc" => [
|
||||
"name" => "Occitan",
|
||||
"nativeName" => "Occitan"
|
||||
],
|
||||
"oj" => [
|
||||
"name" => "Ojibwe, Ojibwa",
|
||||
"nativeName" => "ᐊᓂᔑᓈᐯᒧᐎᓐ"
|
||||
],
|
||||
"cu" => [
|
||||
"name" => "Church Slavic",
|
||||
"nativeName" => "ѩзыкъ словѣньскъ"
|
||||
],
|
||||
"om" => [
|
||||
"name" => "Oromo",
|
||||
"nativeName" => "Afaan Oromoo"
|
||||
],
|
||||
"or" => [
|
||||
"name" => "Oriya",
|
||||
"nativeName" => "ଓଡ଼ିଆ"
|
||||
],
|
||||
"os" => [
|
||||
"name" => "Ossetian",
|
||||
"nativeName" => "ирон æвзаг"
|
||||
],
|
||||
"pa" => [
|
||||
"name" => "Panjabi",
|
||||
"nativeName" => "ਪੰਜਾਬੀ"
|
||||
],
|
||||
"pi" => [
|
||||
"name" => "Pāli",
|
||||
"nativeName" => "पाऴि"
|
||||
],
|
||||
"fa" => [
|
||||
"name" => "Persian",
|
||||
"nativeName" => "فارسی"
|
||||
],
|
||||
"pl" => [
|
||||
"name" => "Polish",
|
||||
"nativeName" => "polski"
|
||||
],
|
||||
"ps" => [
|
||||
"name" => "Pashto",
|
||||
"nativeName" => "پښتو"
|
||||
],
|
||||
"pt" => [
|
||||
"name" => "Portuguese",
|
||||
"nativeName" => "Português"
|
||||
],
|
||||
"qu" => [
|
||||
"name" => "Quechua",
|
||||
"nativeName" => "Runa Simi"
|
||||
],
|
||||
"rm" => [
|
||||
"name" => "Romansh",
|
||||
"nativeName" => "rumantsch grischun"
|
||||
],
|
||||
"rn" => [
|
||||
"name" => "Kirundi",
|
||||
"nativeName" => "kiRundi"
|
||||
],
|
||||
"ro" => [
|
||||
"name" => "Romanian",
|
||||
"nativeName" => "română"
|
||||
],
|
||||
"ru" => [
|
||||
"name" => "Russian",
|
||||
"nativeName" => "Русский"
|
||||
],
|
||||
"sa" => [
|
||||
"name" => "Sanskrit",
|
||||
"nativeName" => "संस्कृतम्"
|
||||
],
|
||||
"sc" => [
|
||||
"name" => "Sardinian",
|
||||
"nativeName" => "sardu"
|
||||
],
|
||||
"sd" => [
|
||||
"name" => "Sindhi",
|
||||
"nativeName" => "सिन्धी"
|
||||
],
|
||||
"se" => [
|
||||
"name" => "Northern Sami",
|
||||
"nativeName" => "Davvisámegiella"
|
||||
],
|
||||
"sm" => [
|
||||
"name" => "Samoan",
|
||||
"nativeName" => "gagana faa Samoa"
|
||||
],
|
||||
"sg" => [
|
||||
"name" => "Sango",
|
||||
"nativeName" => "yângâ tî sängö"
|
||||
],
|
||||
"sr" => [
|
||||
"name" => "Serbian",
|
||||
"nativeName" => "српски језик"
|
||||
],
|
||||
"gd" => [
|
||||
"name" => "Scottish Gaelic",
|
||||
"nativeName" => "Gàidhlig"
|
||||
],
|
||||
"sn" => [
|
||||
"name" => "Shona",
|
||||
"nativeName" => "chiShona"
|
||||
],
|
||||
"si" => [
|
||||
"name" => "Sinhala",
|
||||
"nativeName" => "සිංහල"
|
||||
],
|
||||
"sk" => [
|
||||
"name" => "Slovak",
|
||||
"nativeName" => "slovenčina"
|
||||
],
|
||||
"sl" => [
|
||||
"name" => "Slovene",
|
||||
"nativeName" => "slovenščina"
|
||||
],
|
||||
"so" => [
|
||||
"name" => "Somali",
|
||||
"nativeName" => "Soomaaliga"
|
||||
],
|
||||
"st" => [
|
||||
"name" => "Southern Sotho",
|
||||
"nativeName" => "Sesotho"
|
||||
],
|
||||
"es" => [
|
||||
"name" => "Spanish",
|
||||
"nativeName" => "Español"
|
||||
],
|
||||
"su" => [
|
||||
"name" => "Sundanese",
|
||||
"nativeName" => "Basa Sunda"
|
||||
],
|
||||
"sw" => [
|
||||
"name" => "Swahili",
|
||||
"nativeName" => "Kiswahili"
|
||||
],
|
||||
"ss" => [
|
||||
"name" => "Swati",
|
||||
"nativeName" => "SiSwati"
|
||||
],
|
||||
"sv" => [
|
||||
"name" => "Swedish",
|
||||
"nativeName" => "svenska"
|
||||
],
|
||||
"ta" => [
|
||||
"name" => "Tamil",
|
||||
"nativeName" => "தமிழ்"
|
||||
],
|
||||
"te" => [
|
||||
"name" => "Telugu",
|
||||
"nativeName" => "తెలుగు"
|
||||
],
|
||||
"tg" => [
|
||||
"name" => "Tajik",
|
||||
"nativeName" => "тоҷикӣ"
|
||||
],
|
||||
"th" => [
|
||||
"name" => "Thai",
|
||||
"nativeName" => "ไทย"
|
||||
],
|
||||
"ti" => [
|
||||
"name" => "Tigrinya",
|
||||
"nativeName" => "ትግርኛ"
|
||||
],
|
||||
"bo" => [
|
||||
"name" => "Tibetan",
|
||||
"nativeName" => "བོད་ཡིག"
|
||||
],
|
||||
"tk" => [
|
||||
"name" => "Turkmen",
|
||||
"nativeName" => "Türkmen"
|
||||
],
|
||||
"tl" => [
|
||||
"name" => "Tagalog",
|
||||
"nativeName" => "Wikang Tagalog"
|
||||
],
|
||||
"tn" => [
|
||||
"name" => "Tswana",
|
||||
"nativeName" => "Setswana"
|
||||
],
|
||||
"to" => [
|
||||
"name" => "Tonga",
|
||||
"nativeName" => "faka Tonga"
|
||||
],
|
||||
"tr" => [
|
||||
"name" => "Turkish",
|
||||
"nativeName" => "Türkçe"
|
||||
],
|
||||
"ts" => [
|
||||
"name" => "Tsonga",
|
||||
"nativeName" => "Xitsonga"
|
||||
],
|
||||
"tt" => [
|
||||
"name" => "Tatar",
|
||||
"nativeName" => "татарча"
|
||||
],
|
||||
"tw" => [
|
||||
"name" => "Twi",
|
||||
"nativeName" => "Twi"
|
||||
],
|
||||
"ty" => [
|
||||
"name" => "Tahitian",
|
||||
"nativeName" => "Reo Tahiti"
|
||||
],
|
||||
"ug" => [
|
||||
"name" => "Uighur",
|
||||
"nativeName" => "Uyƣurqə"
|
||||
],
|
||||
"uk" => [
|
||||
"name" => "Ukrainian",
|
||||
"nativeName" => "українська"
|
||||
],
|
||||
"ur" => [
|
||||
"name" => "Urdu",
|
||||
"nativeName" => "اردو"
|
||||
],
|
||||
"uz" => [
|
||||
"name" => "Uzbek",
|
||||
"nativeName" => "zbek"
|
||||
],
|
||||
"ve" => [
|
||||
"name" => "Venda",
|
||||
"nativeName" => "Tshivenḓa"
|
||||
],
|
||||
"vi" => [
|
||||
"name" => "Vietnamese",
|
||||
"nativeName" => "Tiếng Việt"
|
||||
],
|
||||
"vo" => [
|
||||
"name" => "Volapük",
|
||||
"nativeName" => "Volapük"
|
||||
],
|
||||
"wa" => [
|
||||
"name" => "Walloon",
|
||||
"nativeName" => "Walon"
|
||||
],
|
||||
"cy" => [
|
||||
"name" => "Welsh",
|
||||
"nativeName" => "Cymraeg"
|
||||
],
|
||||
"wo" => [
|
||||
"name" => "Wolof",
|
||||
"nativeName" => "Wollof"
|
||||
],
|
||||
"fy" => [
|
||||
"name" => "Western Frisian",
|
||||
"nativeName" => "Frysk"
|
||||
],
|
||||
"xh" => [
|
||||
"name" => "Xhosa",
|
||||
"nativeName" => "isiXhosa"
|
||||
],
|
||||
"yi" => [
|
||||
"name" => "Yiddish",
|
||||
"nativeName" => "ייִדיש"
|
||||
],
|
||||
"yo" => [
|
||||
"name" => "Yoruba",
|
||||
"nativeName" => "Yorùbá"
|
||||
],
|
||||
"za" => [
|
||||
"name" => "Zhuang",
|
||||
"nativeName" => "Saɯ cueŋƅ"
|
||||
]
|
||||
];
|
||||
|
||||
public static function getName($code)
|
||||
{
|
||||
return static::get($code, 'name');
|
||||
}
|
||||
|
||||
public static function getNativeName($code)
|
||||
{
|
||||
if (strlen($code) == 2) {
|
||||
return static::get($code, 'nativeName');
|
||||
} else {
|
||||
return static::get(substr($code, 0, 2), 'nativeName') . ' (' . substr($code, -2) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
/**
|
||||
* Class Parsedown
|
||||
* @package Grav\Common\Markdown
|
||||
*/
|
||||
class Parsedown extends \Parsedown
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
/**
|
||||
* Parsedown constructor.
|
||||
*
|
||||
* @param $page
|
||||
* @param $defaults
|
||||
*/
|
||||
public function __construct($page, $defaults)
|
||||
{
|
||||
$this->init($page, $defaults);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
/**
|
||||
* Class ParsedownExtra
|
||||
* @package Grav\Common\Markdown
|
||||
*/
|
||||
class ParsedownExtra extends \ParsedownExtra
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
/**
|
||||
* ParsedownExtra constructor.
|
||||
*
|
||||
* @param $page
|
||||
* @param $defaults
|
||||
*/
|
||||
public function __construct($page, $defaults)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->init($page, $defaults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
namespace Grav\Common\Markdown;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Uri;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
|
||||
*/
|
||||
trait ParsedownGravTrait
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/** @var Page $page */
|
||||
protected $page;
|
||||
|
||||
/** @var Pages $pages */
|
||||
protected $pages;
|
||||
|
||||
/** @var Uri $uri */
|
||||
protected $uri;
|
||||
|
||||
protected $pages_dir;
|
||||
protected $special_chars;
|
||||
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
|
||||
protected $special_protocols = ['xmpp', 'mailto', 'tel', 'sms'];
|
||||
|
||||
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 = self::getGrav();
|
||||
|
||||
$this->page = $page;
|
||||
$this->pages = $grav['pages'];
|
||||
$this->uri = $grav['uri'];
|
||||
$this->BlockTypes['{'] [] = "TwigTag";
|
||||
$this->pages_dir = self::getGrav()['locator']->findResource('page://');
|
||||
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
|
||||
|
||||
if ($defaults === null) {
|
||||
$defaults = self::getGrav()['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
|
||||
*/
|
||||
public function addBlockType($type, $tag, $continuable = false, $completable = false)
|
||||
{
|
||||
$this->BlockTypes[$type] [] = $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
|
||||
*/
|
||||
public function addInlineType($type, $tag)
|
||||
{
|
||||
$this->InlineTypes[$type] [] = $tag;
|
||||
$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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
protected function blockTwigTag($Line)
|
||||
{
|
||||
if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $Line['body'], $matches)) {
|
||||
$Block = [
|
||||
'markup' => $Line['body'],
|
||||
];
|
||||
|
||||
return $Block;
|
||||
}
|
||||
}
|
||||
|
||||
protected function inlineSpecialCharacter($Excerpt)
|
||||
{
|
||||
if ($Excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $Excerpt['text'])) {
|
||||
return [
|
||||
'markup' => '&',
|
||||
'extent' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->special_chars[$Excerpt['text'][0]])) {
|
||||
return [
|
||||
'markup' => '&' . $this->special_chars[$Excerpt['text'][0]] . ';',
|
||||
'extent' => 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
$excerpt['type'] = 'image';
|
||||
$excerpt = parent::inlineImage($excerpt);
|
||||
}
|
||||
|
||||
// Some stuff we will need
|
||||
$actions = [];
|
||||
$media = null;
|
||||
|
||||
// if this is an image
|
||||
if (isset($excerpt['element']['attributes']['src'])) {
|
||||
$alt = $excerpt['element']['attributes']['alt'] ?: '';
|
||||
$title = $excerpt['element']['attributes']['title'] ?: '';
|
||||
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
|
||||
|
||||
//get the url and parse it
|
||||
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
|
||||
|
||||
$this_host = isset($url['host']) && $url['host'] == $this->uri->host();
|
||||
|
||||
// if there is no host set but there is a path, the file is local
|
||||
if ((!isset($url['host']) || $this_host) && isset($url['path'])) {
|
||||
$path_parts = pathinfo($url['path']);
|
||||
|
||||
// get the local path to page media if possible
|
||||
if ($path_parts['dirname'] == $this->page->url(false, false, false)) {
|
||||
// get the media objects for this page
|
||||
$media = $this->page->media();
|
||||
} else {
|
||||
// see if this is an external page to this one
|
||||
$base_url = rtrim(self::getGrav()['base_url_relative'] . self::getGrav()['pages']->base(), '/');
|
||||
$page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/');
|
||||
|
||||
$ext_page = $this->pages->dispatch($page_route, true);
|
||||
if ($ext_page) {
|
||||
$media = $ext_page->media();
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a media file that matches the path referenced..
|
||||
if ($media && isset($media->all()[$path_parts['basename']])) {
|
||||
// get the medium object
|
||||
$medium = $media->all()[$path_parts['basename']];
|
||||
|
||||
// if there is a query, then parse it and build action calls
|
||||
if (isset($url['query'])) {
|
||||
$actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = isset($parts[1]) ? $parts[1] : null;
|
||||
$carry[] = ['method' => $parts[0], 'params' => $value];
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// loop through actions for the image and call them
|
||||
foreach ($actions as $action) {
|
||||
$medium = call_user_func_array([$medium, $action['method']],
|
||||
explode(',', urldecode($action['params'])));
|
||||
}
|
||||
|
||||
if (isset($url['fragment'])) {
|
||||
$medium->urlHash($url['fragment']);
|
||||
}
|
||||
|
||||
$excerpt['element'] = $medium->parseDownElement($title, $alt, $class, true);
|
||||
|
||||
} else {
|
||||
// not a current page media file, see if it needs converting to relative
|
||||
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
$excerpt = parent::inlineLink($excerpt);
|
||||
}
|
||||
|
||||
// if this is a link
|
||||
if (isset($excerpt['element']['attributes']['href'])) {
|
||||
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href']));
|
||||
|
||||
// if there is a query, then parse it and build action calls
|
||||
if (isset($url['query'])) {
|
||||
$actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
|
||||
$parts = explode('=', $item, 2);
|
||||
$value = isset($parts[1]) ? $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)) {
|
||||
// support both class and classes
|
||||
if ($attrib == 'classes') {
|
||||
$attrib = 'class';
|
||||
}
|
||||
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
|
||||
unset($actions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$url['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
// if no query elements left, unset query
|
||||
if (empty($url['query'])) {
|
||||
unset ($url['query']);
|
||||
}
|
||||
|
||||
// set path to / if not set
|
||||
if (empty($url['path'])) {
|
||||
$url['path'] = '';
|
||||
}
|
||||
|
||||
// if special scheme, just return
|
||||
if(isset($url['scheme']) && in_array($url['scheme'], $this->special_protocols)) {
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
// handle paths and such
|
||||
$url = Uri::convertUrl($this->page, $url, $type);
|
||||
|
||||
// build the URL from the component parts and set it on the element
|
||||
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Iterator;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Collection of Pages.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Create a copy of this collection
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function copy()
|
||||
{
|
||||
return new static($this->items, $this->params, $this->pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from the list.
|
||||
*
|
||||
* @param Page|string|null $key
|
||||
*
|
||||
* @return $this|void
|
||||
* @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
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function order($by, $dir = 'asc', $manual = null)
|
||||
{
|
||||
$this->items = $this->pages->sortCollection($this, $by, $dir, $manual);
|
||||
|
||||
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 Page Item in the array the the current position.
|
||||
*/
|
||||
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) : strtotime("now +1000 years");
|
||||
|
||||
$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 && $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use RocketTheme\Toolbox\ArrayTraits\Constructor;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccess;
|
||||
|
||||
/**
|
||||
* Class Header
|
||||
* @package Grav\Common\Page
|
||||
*/
|
||||
class Header implements \ArrayAccess
|
||||
{
|
||||
use NestedArrayAccess, Constructor;
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use Grav\Common\Getters;
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
|
||||
/**
|
||||
* Media is a holder object that contains references to the media of page. This object is created and
|
||||
* populated during the getMedia() method in the Pages object
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Media extends Getters
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected $gettersVariable = 'instances';
|
||||
protected $path;
|
||||
|
||||
protected $instances = [];
|
||||
protected $images = [];
|
||||
protected $videos = [];
|
||||
protected $audios = [];
|
||||
protected $files = [];
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
*/
|
||||
public function __construct($path)
|
||||
{
|
||||
// Handle special cases where page doesn't exist in filesystem.
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
|
||||
$iterator = new \FilesystemIterator($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->getBasename() === '.DS_Store') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find out what type we're dealing with
|
||||
list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename());
|
||||
|
||||
$media["{$basename}.{$ext}"] = isset($media["{$basename}.{$ext}"]) ? $media["{$basename}.{$ext}"] : [];
|
||||
|
||||
if ($type === 'alternative') {
|
||||
$media["{$basename}.{$ext}"][$type] = isset($media["{$basename}.{$ext}"][$type]) ? $media["{$basename}.{$ext}"][$type] : [];
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the base medium
|
||||
if (!empty($types['base'])) {
|
||||
$medium = MediumFactory::fromFile($types['base']['file']);
|
||||
$medium && $medium->set('size', $types['base']['size']);
|
||||
} else if (!empty($types['alternative'])) {
|
||||
$altMedium = reset($types['alternative']);
|
||||
$ratio = key($types['alternative']);
|
||||
|
||||
$altMedium = $altMedium['file'];
|
||||
|
||||
$medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1)['file'];
|
||||
}
|
||||
|
||||
if (!$medium) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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=2; $i < $max; $i++) {
|
||||
if (isset($alternatives[$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i);
|
||||
}
|
||||
|
||||
foreach ($types['alternative'] as $ratio => $altMedium) {
|
||||
$medium->addAlternative($ratio, $altMedium['file']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->add($name, $medium);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get medium by filename.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return Medium|null
|
||||
*/
|
||||
public function get($filename)
|
||||
{
|
||||
return isset($this->instances[$filename]) ? $this->instances[$filename] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all media.
|
||||
*
|
||||
* @return array|Medium[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
ksort($this->instances, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $this->instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all image media.
|
||||
*
|
||||
* @return array|Medium[]
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
ksort($this->images, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all video media.
|
||||
*
|
||||
* @return array|Medium[]
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
ksort($this->videos, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $this->videos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all audio media.
|
||||
*
|
||||
* @return array|Medium[]
|
||||
*/
|
||||
public function audios()
|
||||
{
|
||||
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $this->audios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all file media.
|
||||
*
|
||||
* @return array|Medium[]
|
||||
*/
|
||||
public function files()
|
||||
{
|
||||
ksort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename, extension and meta part.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return array
|
||||
*/
|
||||
protected function getFileParts($filename)
|
||||
{
|
||||
$fileParts = explode('.', $filename);
|
||||
|
||||
$name = array_shift($fileParts);
|
||||
$type = 'base';
|
||||
$extra = null;
|
||||
|
||||
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 {
|
||||
$extension = null;
|
||||
while (($part = array_shift($fileParts)) !== null) {
|
||||
if ($part != 'meta' && $part != 'thumb') {
|
||||
if (isset($extension)) {
|
||||
$name .= '.' . $extension;
|
||||
}
|
||||
$extension = $part;
|
||||
} else {
|
||||
$type = $part;
|
||||
$extra = '.' . $part . '.' . implode('.', $fileParts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array($name, $extension, $type, $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset medium.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
parent::reset();
|
||||
|
||||
$this->attributes['controls'] = true;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Gregwar\Image\Exceptions\GenerationError;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use Gregwar\Image\Image;
|
||||
|
||||
class ImageFile extends Image
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* 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 mixed|string
|
||||
*/
|
||||
public function cacheFile($type = 'jpg', $quality = 80, $actual = false)
|
||||
{
|
||||
if ($type == 'guess') {
|
||||
$type = $this->guessType();
|
||||
}
|
||||
|
||||
if (!count($this->operations) && $type == $this->guessType() && !$this->forceCache) {
|
||||
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);
|
||||
}
|
||||
|
||||
self::getGrav()->fireEvent('onImageMediumSaved', new Event(['image' => $target]));
|
||||
};
|
||||
|
||||
// Asking the cache for the cacheFile
|
||||
try {
|
||||
$perms = self::getGrav()['config']->get('system.images.cache_perms', '0755');
|
||||
$perms = octdec($perms);
|
||||
$file = $this->cache->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
|
||||
} catch (GenerationError $e) {
|
||||
$file = $e->getNewFile();
|
||||
}
|
||||
|
||||
if ($actual) {
|
||||
return $file;
|
||||
} else {
|
||||
return $this->getFilename($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
|
||||
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'
|
||||
];
|
||||
|
||||
/**
|
||||
* @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 array
|
||||
*/
|
||||
protected $derivatives = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $sizes = '100vw';
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param array $items
|
||||
* @param Blueprint $blueprint
|
||||
*/
|
||||
public function __construct($items = [], Blueprint $blueprint = null)
|
||||
{
|
||||
parent::__construct($items, $blueprint);
|
||||
|
||||
$config = self::$grav['config'];
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$image_path = self::$grav['locator']->findResource('cache://images', true);
|
||||
$image_dir = self::$grav['locator']->findResource('cache://images', false);
|
||||
$saved_image_path = $this->saveImage();
|
||||
|
||||
$output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $saved_image_path);
|
||||
|
||||
if (Utils::startsWith($output, $image_path)) {
|
||||
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path) . '|', '', $output);
|
||||
}
|
||||
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return self::$grav['base_url'] . $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) && empty($this->derivatives)) {
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!empty($this->derivatives)) {
|
||||
asort($this->derivatives);
|
||||
|
||||
foreach ($this->derivatives as $url => $width) {
|
||||
$srcset[] = $url . ' ' . $width . 'w';
|
||||
}
|
||||
|
||||
$srcset[] = $this->url($reset) . ' ' . $this->get('width') . 'w';
|
||||
}
|
||||
else {
|
||||
$srcset = [ $this->url($reset) . ' ' . $this->get('width') . 'w' ];
|
||||
foreach ($this->alternatives as $ratio => $medium) {
|
||||
$srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $srcset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate derivatives
|
||||
*
|
||||
* @param int $min_width
|
||||
* @param int $max_width
|
||||
* @param int $step
|
||||
* @return $this
|
||||
*/
|
||||
public function derivatives($min_width, $max_width, $step = 200) {
|
||||
$width = $min_width;
|
||||
|
||||
// Do not upscale images.
|
||||
if ($max_width > $this->get('width')) {
|
||||
$max_width = $this->get('width');
|
||||
}
|
||||
|
||||
while ($width <= $max_width) {
|
||||
$ratio = $width / $this->get('width');
|
||||
$derivative = MediumFactory::scaledFromMedium($this, 1, $ratio);
|
||||
if (is_array($derivative)) {
|
||||
$this->addDerivative($derivative['file']);
|
||||
}
|
||||
$width += $step;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a derivative
|
||||
*
|
||||
* @param ImageMedium $image
|
||||
*/
|
||||
public function addDerivative(ImageMedium $image) {
|
||||
$this->derivatives[$image->url()] = $image->get('width');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->image->clearOperations(); // Clear previously applied operations
|
||||
$this->querystring('');
|
||||
$this->filter();
|
||||
}
|
||||
|
||||
$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 $this
|
||||
*/
|
||||
public function sizes($sizes = null)
|
||||
{
|
||||
|
||||
if ($sizes) {
|
||||
$this->sizes = $sizes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
return empty($this->sizes) ? '100vw' : $this->sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the width attribute from Markdown or Twig
|
||||
* Examples: 
|
||||
* 
|
||||
* 
|
||||
* 
|
||||
* {{ page.media['myimg.png'].width().height().html }}
|
||||
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
||||
*
|
||||
* @param mixed $value A value or 'auto' or empty to use the width of the image
|
||||
* @return $this
|
||||
*/
|
||||
public function width($value = 'auto')
|
||||
{
|
||||
if (!$value || $value == 'auto')
|
||||
$this->attributes['width'] = $this->get('width');
|
||||
else
|
||||
$this->attributes['width'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set the height attribute from Markdown or Twig
|
||||
* Examples: 
|
||||
* 
|
||||
* 
|
||||
* 
|
||||
* {{ page.media['myimg.png'].width().height().html }}
|
||||
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
||||
*
|
||||
* @param mixed $value A value or 'auto' or empty to use the height of the image
|
||||
* @return $this
|
||||
*/
|
||||
public function height($value = 'auto')
|
||||
{
|
||||
if (!$value || $value == 'auto')
|
||||
$this->attributes['height'] = $this->get('height');
|
||||
else
|
||||
$this->attributes['height'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the call to the image processing method.
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $args
|
||||
* @return $this|mixed
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if ($method == 'cropZoom') {
|
||||
$method = 'zoomCrop';
|
||||
}
|
||||
|
||||
if (!in_array($method, self::$magic_actions)) {
|
||||
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 $ratio => $medium) {
|
||||
$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] = (int) $args_copy[$param] * $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 = self::$grav['locator'];
|
||||
|
||||
$file = $this->get('filepath');
|
||||
$cacheDir = $locator->findResource('cache://images', true);
|
||||
|
||||
$this->image = ImageFile::open($file)
|
||||
->setCacheDir($cacheDir)
|
||||
->setActualCacheDir($cacheDir)
|
||||
->setPrettyName(basename($this->get('basename')));
|
||||
|
||||
$this->filter();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the image with cache.
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function saveImage()
|
||||
{
|
||||
if (!$this->image) {
|
||||
return parent::path(false);
|
||||
}
|
||||
|
||||
if (isset($this->result)) {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
if ($this->get('debug') && !$this->debug_watermarked) {
|
||||
$ratio = $this->get('ratio');
|
||||
if (!$ratio) {
|
||||
$ratio = 1;
|
||||
}
|
||||
|
||||
$locator = self::$grav['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;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
|
||||
class Link implements RenderableInterface
|
||||
{
|
||||
use GravTrait;
|
||||
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 boolean $reset
|
||||
* @return array
|
||||
*/
|
||||
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true)
|
||||
{
|
||||
$innerElement = $this->source->parsedownElement($title, $alt, $class, $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 $this|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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
|
||||
/**
|
||||
* The Medium is a general class for multimedia objects in Grav pages, specific implementations will derive from
|
||||
*
|
||||
* @author Grav
|
||||
* @license MIT
|
||||
*
|
||||
*/
|
||||
class Medium extends Data implements RenderableInterface
|
||||
{
|
||||
use GravTrait;
|
||||
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 = [];
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param array $items
|
||||
* @param Blueprint $blueprint
|
||||
*/
|
||||
public function __construct($items = [], Blueprint $blueprint = null)
|
||||
{
|
||||
parent::__construct($items, $blueprint);
|
||||
|
||||
if (self::getGrav()['config']->get('system.media.enable_media_timestamp', true)) {
|
||||
$this->querystring('&' . self::getGrav()['cache']->getKey());
|
||||
}
|
||||
|
||||
$this->def('mime', 'application/octet-stream');
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return just metadata from the Medium object
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function meta()
|
||||
{
|
||||
return new Data($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta file for the medium.
|
||||
*
|
||||
* @param $filepath
|
||||
*/
|
||||
public function addMetaFile($filepath)
|
||||
{
|
||||
$this->merge(CompiledYamlFile::instance($filepath)->content());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$this->alternatives[(float) $ratio] = $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 URL to file.
|
||||
*
|
||||
* @param bool $reset
|
||||
* @return string
|
||||
*/
|
||||
public function url($reset = true)
|
||||
{
|
||||
$output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $this->get('filepath'));
|
||||
|
||||
if ($reset) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return self::$grav['base_url'] . $output . $this->querystring() . $this->urlHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set querystring for the file's url
|
||||
*
|
||||
* @param string $hash
|
||||
* @param boolean $withHash
|
||||
* @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 boolean $reset
|
||||
* @return array
|
||||
*/
|
||||
public function parsedownElement($title = null, $alt = null, $class = 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'];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($attributes['class'])) {
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = $class;
|
||||
} elseif (!empty($this->items['class'])) {
|
||||
$attributes['class'] = $this->items['class'];
|
||||
}
|
||||
}
|
||||
|
||||
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()->reset() : $this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:  or 
|
||||
*
|
||||
* @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: 
|
||||
*
|
||||
* @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: 
|
||||
*
|
||||
* @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) { return urlencode($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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
|
||||
/**
|
||||
* MediumFactory can be used to more easily create various Medium objects from files or arrays, it should
|
||||
* contain most logic for instantiating a Medium object.
|
||||
*
|
||||
* @author Grav
|
||||
* @license MIT
|
||||
*
|
||||
*/
|
||||
class MediumFactory
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
$path = dirname($file);
|
||||
$filename = basename($file);
|
||||
$parts = explode('.', $filename);
|
||||
$ext = array_pop($parts);
|
||||
$basename = implode('.', $parts);
|
||||
|
||||
$config = self::getGrav()['config'];
|
||||
|
||||
$media_params = $config->get("media.".strtolower($ext));
|
||||
if (!$media_params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$params += $media_params;
|
||||
|
||||
// Add default settings for undefined variables.
|
||||
$params += $config->get('media.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 = self::getGrav()['locator'];
|
||||
|
||||
$lookup = $locator->findResources('image://');
|
||||
foreach ($lookup as $lookupPath) {
|
||||
if (is_file($lookupPath . '/' . $params['thumb'])) {
|
||||
$params['thumbnails']['default'] = $lookupPath . '/' . $params['thumb'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public static function scaledFromMedium($medium, $from, $to)
|
||||
{
|
||||
if (! $medium instanceof ImageMedium) {
|
||||
return $medium;
|
||||
}
|
||||
|
||||
if ($to > $from) {
|
||||
return $medium;
|
||||
}
|
||||
|
||||
$ratio = $to / $from;
|
||||
$width = (int) ($medium->get('width') * $ratio);
|
||||
$height = (int) ($medium->get('height') * $ratio);
|
||||
|
||||
$basename = $medium->get('basename');
|
||||
$basename = str_replace('@'.$from.'x', '@'.$to.'x', $basename);
|
||||
|
||||
$debug = $medium->get('debug');
|
||||
$medium->set('debug', false);
|
||||
|
||||
$file = $medium->resize($width, $height)->path();
|
||||
|
||||
$medium->set('debug', $debug);
|
||||
|
||||
$size = filesize($file);
|
||||
|
||||
$medium = self::fromFile($file);
|
||||
$medium->set('size', $size);
|
||||
|
||||
return ['file' => $medium, 'size' => $size];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
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 $class
|
||||
* @param bool $reset
|
||||
* @return string
|
||||
*/
|
||||
public function html($title = null, $alt = null, $class = null, $reset = true)
|
||||
{
|
||||
$element = $this->parsedownElement($title, $alt, $class, $reset);
|
||||
|
||||
if (!$this->parsedown) {
|
||||
$this->parsedown = new Parsedown(null, null);
|
||||
}
|
||||
|
||||
return $this->parsedown->elementToHtml($element);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
/**
|
||||
* Renderable Medium objects can be rendered to HTML markup and Parsedown objects
|
||||
*
|
||||
* @author Grav
|
||||
* @license MIT
|
||||
*
|
||||
*/
|
||||
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 bool $reset
|
||||
* @return string
|
||||
*/
|
||||
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
/**
|
||||
* The Image medium holds information related to an individual image. These are then stored in the Media object.
|
||||
*
|
||||
* @author Grav
|
||||
* @license MIT
|
||||
*
|
||||
*/
|
||||
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' => 'image', 'attributes' => $attributes ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page\Medium;
|
||||
|
||||
trait StaticResizeTrait
|
||||
{
|
||||
/**
|
||||
* Resize media by setting attributes
|
||||
*
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @return Medium
|
||||
*/
|
||||
public function resize($width = null, $height = null)
|
||||
{
|
||||
$this->styleAttributes['width'] = $width . 'px';
|
||||
$this->styleAttributes['height'] = $height . 'px';
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
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 boolean $reset
|
||||
* @return array
|
||||
*/
|
||||
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true)
|
||||
{
|
||||
return $this->bubble('parsedownElement', [$title, $alt, $class, $reset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $this->bubble('html', [$title, $alt, $class, $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;
|
||||
} else {
|
||||
return call_user_func_array(array($this, 'parent::' . $method), $arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace Grav\Common\Page;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
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;
|
||||
|
||||
class Types implements \ArrayAccess, \Iterator, \Countable
|
||||
{
|
||||
use ArrayAccess, Constructor, Iterator, Countable, Export;
|
||||
|
||||
protected $items;
|
||||
protected $systemBlueprints;
|
||||
|
||||
public function register($type, $blueprint = null)
|
||||
{
|
||||
if (!$blueprint && $this->systemBlueprints && isset($this->systemBlueprints[$type])) {
|
||||
$useBlueprint = $this->systemBlueprints[$type];
|
||||
} else {
|
||||
$useBlueprint = $blueprint;
|
||||
}
|
||||
|
||||
if ($blueprint || empty($this->items[$type])) {
|
||||
$this->items[$type] = $useBlueprint;
|
||||
}
|
||||
}
|
||||
|
||||
public function scanBlueprints($paths)
|
||||
{
|
||||
$this->items = $this->findBlueprints($paths) + $this->items;
|
||||
}
|
||||
|
||||
public function scanTemplates($paths)
|
||||
{
|
||||
$options = [
|
||||
'compare' => 'Filename',
|
||||
'pattern' => '|\.html\.twig$|',
|
||||
'filters' => [
|
||||
'value' => '|\.html\.twig$|'
|
||||
],
|
||||
'value' => 'Filename',
|
||||
'recursive' => false
|
||||
];
|
||||
|
||||
if (!$this->systemBlueprints) {
|
||||
$this->systemBlueprints = $this->findBlueprints('blueprints://pages');
|
||||
}
|
||||
|
||||
// register default by default
|
||||
$this->register('default');
|
||||
|
||||
foreach ((array) $paths as $path) {
|
||||
foreach (Folder::all($path, $options) as $type) {
|
||||
$this->register($type);
|
||||
}
|
||||
$modular_path = rtrim($path, '/') . '/modular';
|
||||
if (file_exists($modular_path)) {
|
||||
foreach (Folder::all($modular_path, $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($paths)
|
||||
{
|
||||
$options = [
|
||||
'compare' => 'Filename',
|
||||
'pattern' => '|\.yaml$|',
|
||||
'filters' => [
|
||||
'key' => '|\.yaml$|'
|
||||
],
|
||||
'key' => 'SubPathName',
|
||||
'value' => 'PathName',
|
||||
];
|
||||
|
||||
$list = [];
|
||||
foreach ((array) $paths as $path) {
|
||||
$list += Folder::all($path, $options);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
/**
|
||||
* The Plugin object just holds the id and path to a plugin.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Plugin implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var Grav
|
||||
*/
|
||||
protected $grav;
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
protected $active = true;
|
||||
/**
|
||||
* @var \Grav\Common\string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$this->grav = $grav;
|
||||
$this->config = $config;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function isAdmin()
|
||||
{
|
||||
if (isset($this->grav['admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool $deep Should you use deep or shallow merging
|
||||
* @param array $params Array of additional configuration options to
|
||||
* merge with the plugin settings.
|
||||
*
|
||||
* @return \Grav\Common\Data\Data
|
||||
*/
|
||||
protected function mergeConfig(Page $page, $deep = false, $params = [])
|
||||
{
|
||||
$class_name = $this->name;
|
||||
$class_name_merged = $class_name . '.merged';
|
||||
$defaults = $this->config->get('plugins.' . $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
|
||||
if ($deep) {
|
||||
$header = array_replace_recursive($defaults, $config);
|
||||
} else {
|
||||
$header = array_merge($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
|
||||
if ($deep) {
|
||||
$header = array_replace_recursive($header, $params);
|
||||
} else {
|
||||
$header = array_merge($header, $params);
|
||||
}
|
||||
|
||||
// Return configurations as a new data config class
|
||||
return new Data($header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
$locator = Grav::instance()['locator'];
|
||||
$filename = 'config://plugins/' . $plugin_name . '.yaml';
|
||||
$file = YamlFile::instance($locator->findResource($filename, true, true));
|
||||
$content = Grav::instance()['config']->get('plugins.' . $plugin_name);
|
||||
$file->save($content);
|
||||
$file->free();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
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\Event\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* The Plugins object holds an array of all the plugin objects that
|
||||
* Grav knows about
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Plugins extends Iterator
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* Recurses through the plugins directory creating Plugin objects for each plugin it finds.
|
||||
*
|
||||
* @return array|Plugin[] array of Plugin objects
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
/** @var Config $config */
|
||||
$config = self::getGrav()['config'];
|
||||
$plugins = (array)$config->get('plugins');
|
||||
|
||||
$inflector = self::getGrav()['inflector'];
|
||||
|
||||
/** @var EventDispatcher $events */
|
||||
$events = self::getGrav()['events'];
|
||||
|
||||
foreach ($plugins as $plugin => $data) {
|
||||
if (empty($data['enabled'])) {
|
||||
// Only load enabled plugins.
|
||||
continue;
|
||||
}
|
||||
|
||||
$locator = self::getGrav()['locator'];
|
||||
$filePath = $locator->findResource('plugins://' . $plugin . DS . $plugin . PLUGIN_EXT);
|
||||
if (!is_file($filePath)) {
|
||||
self::getGrav()['log']->addWarning(sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $plugin));
|
||||
continue;
|
||||
}
|
||||
|
||||
require_once $filePath;
|
||||
|
||||
$pluginClassFormat = [
|
||||
'Grav\\Plugin\\' . ucfirst($plugin) . 'Plugin',
|
||||
'Grav\\Plugin\\' . $inflector->camelize($plugin) . 'Plugin'
|
||||
];
|
||||
$pluginClassName = false;
|
||||
|
||||
foreach ($pluginClassFormat as $pluginClass) {
|
||||
if (class_exists($pluginClass)) {
|
||||
$pluginClassName = $pluginClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $pluginClassName) {
|
||||
throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.",
|
||||
$plugin));
|
||||
}
|
||||
|
||||
$instance = new $pluginClassName($plugin, self::getGrav(), $config);
|
||||
if ($instance instanceof EventSubscriberInterface) {
|
||||
$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()
|
||||
{
|
||||
$list = [];
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$plugins = (array)$locator->findResources('plugins://', false);
|
||||
foreach ($plugins as $path) {
|
||||
$iterator = new \DirectoryIterator($path);
|
||||
|
||||
/** @var \DirectoryIterator $directory */
|
||||
foreach ($iterator as $directory) {
|
||||
if (!$directory->isDir() || $directory->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin = $directory->getBasename();
|
||||
$result = self::get($plugin);
|
||||
|
||||
if ($result) {
|
||||
$list[$plugin] = $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($list);
|
||||
|
||||
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");
|
||||
$blueprint->name = $name;
|
||||
|
||||
// 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(self::getGrav()['config']->get('plugins.' . $name) ?: []);
|
||||
|
||||
// Save configuration always to user/config.
|
||||
$file = CompiledYamlFile::instance("config://plugins/{$name}.yaml");
|
||||
$obj->file($file);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
namespace Grav\Common\Service;
|
||||
|
||||
use Grav\Common\Config\CompiledBlueprints;
|
||||
use Grav\Common\Config\CompiledConfig;
|
||||
use Grav\Common\Config\CompiledLanguages;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Config\ConfigFileFinder;
|
||||
use Grav\Common\Config\Setup;
|
||||
use Pimple\Container;
|
||||
use Pimple\ServiceProviderInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* The Config class contains configuration information.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class ConfigServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container)
|
||||
{
|
||||
$container['setup'] = function ($c) {
|
||||
return static::setup($c);
|
||||
};
|
||||
|
||||
$container['blueprints'] = function ($c) {
|
||||
return static::blueprints($c);
|
||||
};
|
||||
|
||||
$container['config'] = function ($c) {
|
||||
return static::load($c);
|
||||
};
|
||||
|
||||
$container['languages'] = function ($c) {
|
||||
return static::languages($c);
|
||||
};
|
||||
}
|
||||
|
||||
public static function setup(Container $container)
|
||||
{
|
||||
return new Setup($container);
|
||||
}
|
||||
|
||||
public static function blueprints(Container $container)
|
||||
{
|
||||
/** Setup $setup */
|
||||
$setup = $container['setup'];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $container['locator'];
|
||||
|
||||
$cache = $locator->findResource('cache://compiled/blueprints', true, true);
|
||||
|
||||
$files = [];
|
||||
$paths = $locator->findResources('blueprints://config');
|
||||
$files += (new ConfigFileFinder)->locateFiles($paths);
|
||||
$paths = $locator->findResources('plugins://');
|
||||
$files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints');
|
||||
|
||||
$blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT);
|
||||
|
||||
return $blueprints->name("master-{$setup->environment}")->load();
|
||||
}
|
||||
|
||||
public static function load(Container $container)
|
||||
{
|
||||
/** Setup $setup */
|
||||
$setup = $container['setup'];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $container['locator'];
|
||||
|
||||
$cache = $locator->findResource('cache://compiled/config', true, true);
|
||||
|
||||
$files = [];
|
||||
$paths = $locator->findResources('config://');
|
||||
$files += (new ConfigFileFinder)->locateFiles($paths);
|
||||
$paths = $locator->findResources('plugins://');
|
||||
$files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths);
|
||||
|
||||
$config = new CompiledConfig($cache, $files, GRAV_ROOT);
|
||||
$config->setBlueprints(function() use ($container) {
|
||||
return $container['blueprints'];
|
||||
});
|
||||
|
||||
return $config->name("master-{$setup->environment}")->load();
|
||||
}
|
||||
|
||||
public static function languages(Container $container)
|
||||
{
|
||||
/** Setup $setup */
|
||||
$setup = $container['setup'];
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $container['config'];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $container['locator'];
|
||||
|
||||
$cache = $locator->findResource('cache://compiled/languages', true, true);
|
||||
$files = [];
|
||||
|
||||
// Process languages only if enabled in configuration.
|
||||
if ($config->get('system.languages.translations', true)) {
|
||||
$paths = $locator->findResources('languages://');
|
||||
$files += (new ConfigFileFinder)->locateFiles($paths);
|
||||
$paths = $locator->findResources('plugins://');
|
||||
$files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages');
|
||||
$paths = static::pluginFolderPaths($paths, 'languages');
|
||||
$files += (new ConfigFileFinder)->locateFiles($paths);
|
||||
}
|
||||
|
||||
$languages = new CompiledLanguages($cache, $files, GRAV_ROOT);
|
||||
|
||||
return $languages->name("master-{$setup->environment}")->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find specific paths in plugins
|
||||
*
|
||||
* @param $plugins
|
||||
* @param $folder_path
|
||||
* @return array
|
||||
*/
|
||||
private static function pluginFolderPaths($plugins, $folder_path)
|
||||
{
|
||||
$paths = [];
|
||||
|
||||
foreach ($plugins as $path) {
|
||||
$iterator = new \DirectoryIterator($path);
|
||||
|
||||
/** @var \DirectoryIterator $directory */
|
||||
foreach ($iterator as $directory) {
|
||||
if (!$directory->isDir() || $directory->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Path to the languages folder
|
||||
$lang_path = $directory->getPathName() . '/' . $folder_path;
|
||||
|
||||
// If this folder exists, add it to the list of paths
|
||||
if (file_exists($lang_path)) {
|
||||
$paths []= $lang_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $paths;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Grav\Common\Service;
|
||||
|
||||
use Grav\Common\Errors\Errors;
|
||||
use Pimple\Container;
|
||||
use Pimple\ServiceProviderInterface;
|
||||
use Whoops\Handler\JsonResponseHandler;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Handler\PlainTextHandler;
|
||||
|
||||
class ErrorServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container)
|
||||
{
|
||||
$errors = new Errors;
|
||||
$container['errors'] = $errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Grav\Common\Service;
|
||||
|
||||
use Pimple\Container;
|
||||
use Pimple\ServiceProviderInterface;
|
||||
use \Monolog\Logger;
|
||||
use \Monolog\Handler\StreamHandler;
|
||||
|
||||
class LoggerServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container)
|
||||
{
|
||||
$log = new Logger('grav');
|
||||
$log_file = LOG_DIR.'grav.log';
|
||||
|
||||
$log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
|
||||
|
||||
$container['log'] = $log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace Grav\Common\Service;
|
||||
|
||||
use Grav\Common\Config\Setup;
|
||||
use Pimple\Container;
|
||||
use RocketTheme\Toolbox\DI\ServiceProviderInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RocketTheme\Toolbox\StreamWrapper\ReadOnlyStream;
|
||||
use RocketTheme\Toolbox\StreamWrapper\Stream;
|
||||
use RocketTheme\Toolbox\StreamWrapper\StreamBuilder;
|
||||
|
||||
class StreamsServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container)
|
||||
{
|
||||
$container['locator'] = function($c) {
|
||||
$locator = new UniformResourceLocator(GRAV_ROOT);
|
||||
|
||||
/** @var Setup $setup */
|
||||
$setup = $c['setup'];
|
||||
$setup->initializeLocator($locator);
|
||||
|
||||
return $locator;
|
||||
};
|
||||
|
||||
$container['streams'] = function($c) {
|
||||
/** @var Setup $setup */
|
||||
$setup = $c['setup'];
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $c['locator'];
|
||||
|
||||
// Set locator to both streams.
|
||||
Stream::setLocator($locator);
|
||||
ReadOnlyStream::setLocator($locator);
|
||||
|
||||
return new StreamBuilder($setup->getStreams($c));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
/**
|
||||
* Wrapper for Session
|
||||
*/
|
||||
class Session extends \RocketTheme\Toolbox\Session\Session
|
||||
{
|
||||
protected $grav;
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* Session constructor.
|
||||
*
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
$this->grav = $grav;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session init
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
/** @var Uri $uri */
|
||||
$uri = $this->grav['uri'];
|
||||
$config = $this->grav['config'];
|
||||
|
||||
$is_admin = false;
|
||||
|
||||
$session_timeout = $config->get('system.session.timeout', 1800);
|
||||
$session_path = $config->get('system.session.path', '/' . ltrim($uri->rootUrl(false), '/'));
|
||||
|
||||
// Activate admin if we're inside the admin path.
|
||||
if ($config->get('plugins.admin.enabled')) {
|
||||
$route = $config->get('plugins.admin.route');
|
||||
$base = '/' . trim($route, '/');
|
||||
if (substr($uri->route(), 0, strlen($base)) == $base) {
|
||||
$session_timeout = $config->get('plugins.admin.session.timeout', 1800);
|
||||
$is_admin = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->get('system.session.enabled') || $is_admin) {
|
||||
// Define session service.
|
||||
parent::__construct($session_timeout, $session_path);
|
||||
|
||||
$domain = $uri->host();
|
||||
if ($domain == 'localhost') {
|
||||
$domain = '';
|
||||
}
|
||||
$secure = $config->get('system.session.secure', false);
|
||||
$httponly = $config->get('system.session.httponly', true);
|
||||
|
||||
$unique_identifier = GRAV_ROOT;
|
||||
$this->setName($config->get('system.session.name', 'grav_site') . '-' . substr(md5($unique_identifier), 0, 7) . ($is_admin ? '-admin' : ''));
|
||||
$this->start();
|
||||
setcookie(session_name(), session_id(), time() + $session_timeout, $session_path, $domain, $secure, $httponly);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Page\Collection;
|
||||
use Grav\Common\Page\Page;
|
||||
|
||||
/**
|
||||
* The Taxonomy object is a singleton that holds a reference to a 'taxonomy map'. This map is
|
||||
* constructed as a multidimensional array.
|
||||
*
|
||||
* uses the taxonomy defined in the site.yaml file and is built when the page objects are recursed.
|
||||
* Basically every time a page is found that has taxonomy references, an entry to the page is stored in
|
||||
* the taxonomy map. The map has the following format:
|
||||
*
|
||||
* [taxonomy_type][taxonomy_value][page_path]
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* [category][blog][path/to/item1]
|
||||
* [tag][grav][path/to/item1]
|
||||
* [tag][grav][path/to/item2]
|
||||
* [tag][dog][path/to/item3]
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Taxonomy
|
||||
{
|
||||
protected $taxonomy_map;
|
||||
protected $grav;
|
||||
|
||||
/**
|
||||
* Constructor that resets the map
|
||||
*
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
$this->taxonomy_map = [];
|
||||
$this->grav = $grav;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an individual page and processes the taxonomies configured in its header. It
|
||||
* then adds those taxonomies to the map
|
||||
*
|
||||
* @param Page $page the page to process
|
||||
* @param array $page_taxonomy
|
||||
*/
|
||||
public function addTaxonomy(Page $page, $page_taxonomy = null)
|
||||
{
|
||||
if (!$page_taxonomy) {
|
||||
$page_taxonomy = $page->taxonomy();
|
||||
}
|
||||
|
||||
if (!$page->published() || empty($page_taxonomy)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $this->grav['config'];
|
||||
if ($config->get('site.taxonomies')) {
|
||||
foreach ((array)$config->get('site.taxonomies') as $taxonomy) {
|
||||
if (isset($page_taxonomy[$taxonomy])) {
|
||||
foreach ((array)$page_taxonomy[$taxonomy] as $item) {
|
||||
$this->taxonomy_map[$taxonomy][(string)$item][$page->path()] = ['slug' => $page->slug()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Page object with the sub-pages containing all the values set for a
|
||||
* particular taxonomy.
|
||||
*
|
||||
* @param array $taxonomies taxonomies to search, eg ['tag'=>['animal','cat']]
|
||||
* @param string $operator can be 'or' or 'and' (defaults to 'or')
|
||||
*
|
||||
* @return Collection Collection object set to contain matches found in the taxonomy map
|
||||
*/
|
||||
public function findTaxonomy($taxonomies, $operator = 'and')
|
||||
{
|
||||
$matches = [];
|
||||
$results = [];
|
||||
|
||||
foreach ((array)$taxonomies as $taxonomy => $items) {
|
||||
foreach ((array)$items as $item) {
|
||||
if (isset($this->taxonomy_map[$taxonomy][$item])) {
|
||||
$matches[] = $this->taxonomy_map[$taxonomy][$item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strtolower($operator) == 'or') {
|
||||
foreach ($matches as $match) {
|
||||
$results = array_merge($results, $match);
|
||||
}
|
||||
} else {
|
||||
$results = $matches ? array_pop($matches) : [];
|
||||
foreach ($matches as $match) {
|
||||
$results = array_intersect_key($results, $match);
|
||||
}
|
||||
}
|
||||
|
||||
return new Collection($results, ['taxonomies' => $taxonomies]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the taxonomy map
|
||||
*
|
||||
* @param array $var the taxonomy map
|
||||
*
|
||||
* @return array the taxonomy map
|
||||
*/
|
||||
public function taxonomy($var = null)
|
||||
{
|
||||
if ($var) {
|
||||
$this->taxonomy_map = $var;
|
||||
}
|
||||
|
||||
return $this->taxonomy_map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
|
||||
/**
|
||||
* Class Theme
|
||||
* @package Grav\Common
|
||||
*/
|
||||
class Theme extends Plugin
|
||||
{
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Grav $grav
|
||||
* @param Config $config
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct(Grav $grav, Config $config, $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
parent::__construct($name, $grav, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists to disk the theme parameters currently stored in the Grav Config object
|
||||
*
|
||||
* @param string $theme_name The name of the theme whose config it should store.
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public static function saveConfig($theme_name)
|
||||
{
|
||||
if (!$theme_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$locator = Grav::instance()['locator'];
|
||||
$filename = 'config://themes/' . $theme_name . '.yaml';
|
||||
$file = YamlFile::instance($locator->findResource($filename, true, true));
|
||||
$content = Grav::instance()['config']->get('themes.' . $theme_name);
|
||||
$file->save($content);
|
||||
$file->free();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Data\Blueprints;
|
||||
use Grav\Common\Data\Data;
|
||||
use RocketTheme\Toolbox\Event\EventDispatcher;
|
||||
use RocketTheme\Toolbox\Event\EventSubscriberInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* The Themes object holds an array of all the theme objects that Grav knows about.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Themes extends Iterator
|
||||
{
|
||||
/** @var Grav */
|
||||
protected $grav;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Themes constructor.
|
||||
*
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->grav = $grav;
|
||||
$this->config = $grav['config'];
|
||||
|
||||
// Register instance as autoloader for theme inheritance
|
||||
spl_autoload_register([$this, 'autoloadTheme']);
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
/** @var Themes $themes */
|
||||
$themes = $this->grav['themes'];
|
||||
$themes->configure();
|
||||
|
||||
$this->initTheme();
|
||||
}
|
||||
|
||||
public function initTheme()
|
||||
{
|
||||
/** @var Themes $themes */
|
||||
$themes = $this->grav['themes'];
|
||||
|
||||
try {
|
||||
$instance = $themes->load();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new \RuntimeException($this->current() . ' theme could not be found');
|
||||
}
|
||||
|
||||
if ($instance instanceof EventSubscriberInterface) {
|
||||
/** @var EventDispatcher $events */
|
||||
$events = $this->grav['events'];
|
||||
|
||||
$events->addSubscriber($instance);
|
||||
}
|
||||
|
||||
$this->grav['theme'] = $instance;
|
||||
|
||||
$this->grav->fireEvent('onThemeInitialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of all theme data with their blueprints.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$list = [];
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$themes = (array)$locator->findResources('themes://', false);
|
||||
foreach ($themes as $path) {
|
||||
$iterator = new \DirectoryIterator($path);
|
||||
|
||||
/** @var \DirectoryIterator $directory */
|
||||
foreach ($iterator as $directory) {
|
||||
if (!$directory->isDir() || $directory->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$theme = $directory->getBasename();
|
||||
$result = self::get($theme);
|
||||
|
||||
if ($result) {
|
||||
$list[$theme] = $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($list);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme configuration or throw exception if it cannot be found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Data
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function get($name)
|
||||
{
|
||||
if (!$name) {
|
||||
throw new \RuntimeException('Theme name not provided.');
|
||||
}
|
||||
|
||||
$blueprints = new Blueprints('themes://');
|
||||
$blueprint = $blueprints->get("{$name}/blueprints");
|
||||
$blueprint->name = $name;
|
||||
|
||||
// Load default configuration.
|
||||
$file = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT);
|
||||
|
||||
// ensure this is a valid theme
|
||||
if (!$file->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find thumbnail.
|
||||
$thumb = "themes://{$name}/thumbnail.jpg";
|
||||
$path = $this->grav['locator']->findResource($thumb, false);
|
||||
|
||||
if ($path) {
|
||||
$blueprint->set('thumbnail', $this->grav['base_url'] . '/' . $path);
|
||||
}
|
||||
|
||||
$obj = new Data($file->content(), $blueprint);
|
||||
|
||||
// Override with user configuration.
|
||||
$obj->merge($this->grav['config']->get('themes.' . $name) ?: []);
|
||||
|
||||
// Save configuration always to user/config.
|
||||
$file = CompiledYamlFile::instance("config://themes/{$name}" . YAML_EXT);
|
||||
$obj->file($file);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return name of the current theme.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return (string)$this->config->get('system.pages.theme');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current theme.
|
||||
*
|
||||
* @return Theme|object
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
// NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM!
|
||||
$grav = $this->grav;
|
||||
$config = $this->config;
|
||||
$name = $this->current();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
|
||||
|
||||
$inflector = $grav['inflector'];
|
||||
|
||||
if ($file) {
|
||||
// Local variables available in the file: $grav, $config, $name, $file
|
||||
$class = include $file;
|
||||
|
||||
if (!is_object($class)) {
|
||||
$themeClassFormat = [
|
||||
'Grav\\Theme\\' . ucfirst($name),
|
||||
'Grav\\Theme\\' . $inflector->camelize($name)
|
||||
];
|
||||
|
||||
foreach ($themeClassFormat as $themeClass) {
|
||||
if (class_exists($themeClass)) {
|
||||
$themeClassName = $themeClass;
|
||||
$class = new $themeClassName($grav, $config, $name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
|
||||
exit("Theme '$name' does not exist, unable to display page.");
|
||||
}
|
||||
|
||||
$this->config->set('theme', $config->get('themes.' . $name));
|
||||
|
||||
if (empty($class)) {
|
||||
$class = new Theme($grav, $config, $name);
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure and prepare streams for current template.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
$name = $this->current();
|
||||
$config = $this->config;
|
||||
|
||||
$this->loadConfiguration($name, $config);
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
$registered = stream_get_wrappers();
|
||||
$schemes = $config->get(
|
||||
"themes.{$name}.streams.schemes",
|
||||
['theme' => ['paths' => $locator->findResources("themes://{$name}", false)]]
|
||||
);
|
||||
|
||||
foreach ($schemes as $scheme => $config) {
|
||||
if (isset($config['paths'])) {
|
||||
$locator->addPath($scheme, '', $config['paths']);
|
||||
}
|
||||
if (isset($config['prefixes'])) {
|
||||
foreach ($config['prefixes'] as $prefix => $paths) {
|
||||
$locator->addPath($scheme, $prefix, $paths);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($scheme, $registered)) {
|
||||
stream_wrapper_unregister($scheme);
|
||||
}
|
||||
$type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
|
||||
if ($type[0] != '\\') {
|
||||
$type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
|
||||
}
|
||||
|
||||
if (!stream_wrapper_register($scheme, $type)) {
|
||||
throw new \InvalidArgumentException("Stream '{$type}' could not be initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
// Load languages after streams has been properly initialized
|
||||
$this->loadLanguages($this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme configuration.
|
||||
*
|
||||
* @param string $name Theme name
|
||||
* @param Config $config Configuration class
|
||||
*/
|
||||
protected function loadConfiguration($name, Config $config)
|
||||
{
|
||||
$themeConfig = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT)->content();
|
||||
$config->joinDefaults("themes.{$name}", $themeConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme languages.
|
||||
*
|
||||
* @param Config $config Configuration class
|
||||
*/
|
||||
protected function loadLanguages(Config $config)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
if ($config->get('system.languages.translations', true)) {
|
||||
$languageFiles = array_reverse($locator->findResources("theme://languages" . YAML_EXT));
|
||||
|
||||
$languages = [];
|
||||
foreach ($languageFiles as $language) {
|
||||
$languages[] = CompiledYamlFile::instance($language)->content();
|
||||
}
|
||||
|
||||
if ($languages) {
|
||||
$languages = call_user_func_array('array_replace_recursive', $languages);
|
||||
$this->grav['languages']->mergeRecursive($languages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autoload theme classes for inheritance
|
||||
*
|
||||
* @param string $class Class name
|
||||
*
|
||||
* @return mixed false FALSE if unable to load $class; Class name if
|
||||
* $class is successfully loaded
|
||||
*/
|
||||
protected function autoloadTheme($class)
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
$prefix = "Grav\\Theme";
|
||||
if (false !== strpos($class, $prefix)) {
|
||||
// Remove prefix from class
|
||||
$class = substr($class, strlen($prefix));
|
||||
|
||||
// Replace namespace tokens to directory separators
|
||||
$path = strtolower(ltrim(preg_replace('#\\\|_(?!.+\\\)#', '/', $class), '/'));
|
||||
$file = $locator->findResource("themes://{$path}/{$path}.php");
|
||||
|
||||
// Load class
|
||||
if (file_exists($file)) {
|
||||
return include_once($file);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
namespace Grav\Common\Twig;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Page;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* The Twig object handles all the Twig template rendering for Grav. It's a singleton object
|
||||
* that is optimized so that it only needs to be initialized once and can be reused for individual
|
||||
* page template rendering as well as the main site template rendering.
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Twig
|
||||
{
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
*/
|
||||
public $twig;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $twig_vars = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $twig_paths;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $template;
|
||||
|
||||
/**
|
||||
* @var Grav
|
||||
*/
|
||||
protected $grav;
|
||||
|
||||
/**
|
||||
* @var \Twig_Loader_Filesystem
|
||||
*/
|
||||
protected $loader;
|
||||
|
||||
/**
|
||||
* @var \Twig_Loader_Array
|
||||
*/
|
||||
protected $loaderArray;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
$this->grav = $grav;
|
||||
$this->twig_paths = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Twig initialization that sets the twig loader chain, then the environment, then extensions
|
||||
* and also the base set of twig vars
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
if (!isset($this->twig)) {
|
||||
/** @var Config $config */
|
||||
$config = $this->grav['config'];
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $this->grav['language'];
|
||||
|
||||
$active_language = $language->getActive();
|
||||
|
||||
$language_append = '';
|
||||
if ($language->getDefault() != $active_language || $config->get('system.languages.include_default_lang') === true) {
|
||||
$language_append = $active_language ? '/' . $active_language : '';
|
||||
}
|
||||
|
||||
// handle language templates if available
|
||||
if ($language->enabled()) {
|
||||
$lang_templates = $locator->findResource('theme://templates/' . ($active_language ? $active_language : $language->getDefault()));
|
||||
if ($lang_templates) {
|
||||
$this->twig_paths[] = $lang_templates;
|
||||
}
|
||||
}
|
||||
|
||||
$this->twig_paths = array_merge($this->twig_paths, $locator->findResources('theme://templates'));
|
||||
|
||||
$this->grav->fireEvent('onTwigTemplatePaths');
|
||||
|
||||
$this->loader = new \Twig_Loader_Filesystem($this->twig_paths);
|
||||
$this->loaderArray = new \Twig_Loader_Array([]);
|
||||
$loader_chain = new \Twig_Loader_Chain([$this->loaderArray, $this->loader]);
|
||||
|
||||
$params = $config->get('system.twig');
|
||||
if (!empty($params['cache'])) {
|
||||
$params['cache'] = $locator->findResource('cache://twig', true, true);
|
||||
}
|
||||
|
||||
$this->twig = new TwigEnvironment($loader_chain, $params);
|
||||
|
||||
if ($config->get('system.twig.undefined_functions')) {
|
||||
$this->twig->registerUndefinedFunctionCallback(function ($name) {
|
||||
if (function_exists($name)) {
|
||||
return new \Twig_Function_Function($name);
|
||||
}
|
||||
|
||||
return new \Twig_Function_Function(function () {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($config->get('system.twig.undefined_filters')) {
|
||||
$this->twig->registerUndefinedFilterCallback(function ($name) {
|
||||
if (function_exists($name)) {
|
||||
return new \Twig_Filter_Function($name);
|
||||
}
|
||||
|
||||
return new \Twig_Filter_Function(function () {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$this->grav->fireEvent('onTwigInitialized');
|
||||
|
||||
// set default date format if set in config
|
||||
if ($config->get('system.pages.dateformat.long')) {
|
||||
$this->twig->getExtension('core')->setDateFormat($config->get('system.pages.dateformat.long'));
|
||||
}
|
||||
// enable the debug extension if required
|
||||
if ($config->get('system.twig.debug')) {
|
||||
$this->twig->addExtension(new \Twig_Extension_Debug());
|
||||
}
|
||||
$this->twig->addExtension(new TwigExtension());
|
||||
|
||||
$this->grav->fireEvent('onTwigExtensions');
|
||||
|
||||
// Set some standard variables for twig
|
||||
$this->twig_vars = $this->twig_vars + [
|
||||
'config' => $config,
|
||||
'uri' => $this->grav['uri'],
|
||||
'base_dir' => rtrim(ROOT_DIR, '/'),
|
||||
'base_url' => $this->grav['base_url'] . $language_append,
|
||||
'base_url_simple' => $this->grav['base_url'],
|
||||
'base_url_absolute' => $this->grav['base_url_absolute'] . $language_append,
|
||||
'base_url_relative' => $this->grav['base_url_relative'] . $language_append,
|
||||
'theme_dir' => $locator->findResource('theme://'),
|
||||
'theme_url' => $this->grav['base_url'] . '/' . $locator->findResource('theme://', false),
|
||||
'site' => $config->get('site'),
|
||||
'assets' => $this->grav['assets'],
|
||||
'taxonomy' => $this->grav['taxonomy'],
|
||||
'browser' => $this->grav['browser'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Twig_Environment
|
||||
*/
|
||||
public function twig()
|
||||
{
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Twig_Loader_Filesystem
|
||||
*/
|
||||
public function loader()
|
||||
{
|
||||
return $this->loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or overrides a template.
|
||||
*
|
||||
* @param string $name The template name
|
||||
* @param string $template The template source
|
||||
*/
|
||||
public function setTemplate($name, $template)
|
||||
{
|
||||
$this->loaderArray->setTemplate($name, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Twig process that renders a page item. It supports two variations:
|
||||
* 1) Handles modular pages by rendering a specific page based on its modular twig template
|
||||
* 2) Renders individual page items for twig processing before the site rendering
|
||||
*
|
||||
* @param Page $item The page item to render
|
||||
* @param string $content Optional content override
|
||||
*
|
||||
* @return string The rendered output
|
||||
* @throws \Twig_Error_Loader
|
||||
*/
|
||||
public function processPage(Page $item, $content = null)
|
||||
{
|
||||
$content = $content !== null ? $content : $item->content();
|
||||
|
||||
// override the twig header vars for local resolution
|
||||
$this->grav->fireEvent('onTwigPageVariables', new Event(['page' => $item]));
|
||||
$twig_vars = $this->twig_vars;
|
||||
|
||||
$twig_vars['page'] = $item;
|
||||
$twig_vars['media'] = $item->media();
|
||||
$twig_vars['header'] = $item->header();
|
||||
|
||||
$local_twig = clone($this->twig);
|
||||
$modular_twig = $item->modularTwig();
|
||||
$process_twig = isset($item->header()->process['twig']) ? $item->header()->process['twig'] : false;
|
||||
|
||||
$output = '';
|
||||
|
||||
try {
|
||||
// Process Modular Twig
|
||||
if ($modular_twig) {
|
||||
$twig_vars['content'] = $content;
|
||||
$template = $item->template() . TEMPLATE_EXT;
|
||||
$output = $content = $local_twig->render($template, $twig_vars);
|
||||
}
|
||||
|
||||
// Process in-page Twig
|
||||
if (!$modular_twig || ($modular_twig && $process_twig)) {
|
||||
$name = '@Page:' . $item->path();
|
||||
$this->setTemplate($name, $content);
|
||||
$output = $local_twig->render($name, $twig_vars);
|
||||
}
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
throw new \RuntimeException($e->getRawMessage(), 404, $e);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Twig template directly by using a template name
|
||||
* and optional array of variables
|
||||
*
|
||||
* @param string $template template to render with
|
||||
* @param array $vars Optional variables
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function processTemplate($template, $vars = [])
|
||||
{
|
||||
// override the twig header vars for local resolution
|
||||
$this->grav->fireEvent('onTwigTemplateVariables');
|
||||
$vars += $this->twig_vars;
|
||||
|
||||
try {
|
||||
$output = $this->twig->render($template, $vars);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
throw new \RuntimeException($e->getRawMessage(), 404, $e);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a Twig template directly by using a Twig string
|
||||
* and optional array of variables
|
||||
*
|
||||
* @param string $string string to render.
|
||||
* @param array $vars Optional variables
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function processString($string, array $vars = [])
|
||||
{
|
||||
// override the twig header vars for local resolution
|
||||
$this->grav->fireEvent('onTwigStringVariables');
|
||||
$vars += $this->twig_vars;
|
||||
|
||||
$name = '@Var:' . $string;
|
||||
$this->setTemplate($name, $string);
|
||||
|
||||
try {
|
||||
$output = $this->twig->render($name, $vars);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
throw new \RuntimeException($e->getRawMessage(), 404, $e);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twig process that renders the site layout. This is the main twig process that renders the overall
|
||||
* page and handles all the layout for the site display.
|
||||
*
|
||||
* @param string $format Output format (defaults to HTML).
|
||||
*
|
||||
* @return string the rendered output
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function processSite($format = null)
|
||||
{
|
||||
// set the page now its been processed
|
||||
$this->grav->fireEvent('onTwigSiteVariables');
|
||||
$pages = $this->grav['pages'];
|
||||
$page = $this->grav['page'];
|
||||
$content = $page->content();
|
||||
|
||||
$twig_vars = $this->twig_vars;
|
||||
|
||||
$twig_vars['pages'] = $pages->root();
|
||||
$twig_vars['page'] = $page;
|
||||
$twig_vars['header'] = $page->header();
|
||||
$twig_vars['media'] = $page->media();
|
||||
$twig_vars['content'] = $content;
|
||||
$ext = '.' . ($format ? $format : 'html') . TWIG_EXT;
|
||||
|
||||
// determine if params are set, if so disable twig cache
|
||||
$params = $this->grav['uri']->params(null, true);
|
||||
if (!empty($params)) {
|
||||
$this->twig->setCache(false);
|
||||
}
|
||||
|
||||
// Get Twig template layout
|
||||
$template = $this->template($page->template() . $ext);
|
||||
|
||||
try {
|
||||
$output = $this->twig->render($template, $twig_vars);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
$error_msg = $e->getMessage();
|
||||
// Try html version of this template if initial template was NOT html
|
||||
if ($ext != '.html' . TWIG_EXT) {
|
||||
try {
|
||||
$output = $this->twig->render($page->template() . '.html' . TWIG_EXT, $twig_vars);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
throw new \RuntimeException($error_msg, 400, $e);
|
||||
}
|
||||
} else {
|
||||
throw new \RuntimeException($error_msg, 400, $e);
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple helper method to get the twig template if it has already been set, else return
|
||||
* the one being passed in
|
||||
*
|
||||
* @param string $template the template name
|
||||
*
|
||||
* @return string the template name
|
||||
*/
|
||||
public function template($template)
|
||||
{
|
||||
if (isset($this->template)) {
|
||||
return $this->template;
|
||||
} else {
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Grav\Common\Twig;
|
||||
|
||||
/**
|
||||
* The Twig Environment class is a wrapper that handles configurable permissions
|
||||
* for the Twig cache files
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class TwigEnvironment extends \Twig_Environment
|
||||
{
|
||||
use WriteCacheFileTrait;
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
<?php
|
||||
namespace Grav\Common\Twig;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Markdown\Parsedown;
|
||||
use Grav\Common\Markdown\ParsedownExtra;
|
||||
use Grav\Common\Uri;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* The Twig extension adds some filters and functions that are useful for Grav
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class TwigExtension extends \Twig_Extension
|
||||
{
|
||||
protected $grav;
|
||||
protected $debugger;
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* TwigExtension constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->grav = Grav::instance();
|
||||
$this->debugger = isset($this->grav['debugger']) ? $this->grav['debugger'] : null;
|
||||
$this->config = $this->grav['config'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns extension name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'GravTwigExtension';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register some standard globals
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGlobals()
|
||||
{
|
||||
return [
|
||||
'grav' => $this->grav,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all filters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFilter('*ize', [$this, 'inflectorFilter']),
|
||||
new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
|
||||
new \Twig_SimpleFilter('contains', [$this, 'containsFilter']),
|
||||
new \Twig_SimpleFilter('defined', [$this, 'definedDefaultFilter']),
|
||||
new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter']),
|
||||
new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']),
|
||||
new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']),
|
||||
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
|
||||
new \Twig_SimpleFilter('markdown', [$this, 'markdownFilter']),
|
||||
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
|
||||
new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
|
||||
new \Twig_SimpleFilter('randomize', [$this, 'randomizeFilter']),
|
||||
new \Twig_SimpleFilter('modulus', [$this, 'modulusFilter']),
|
||||
new \Twig_SimpleFilter('rtrim', [$this, 'rtrimFilter']),
|
||||
new \Twig_SimpleFilter('pad', [$this, 'padFilter']),
|
||||
new \Twig_SimpleFilter('safe_email', [$this, 'safeEmailFilter']),
|
||||
new \Twig_SimpleFilter('safe_truncate', ['\Grav\Common\Utils', 'safeTruncate']),
|
||||
new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']),
|
||||
new \Twig_SimpleFilter('sort_by_key', [$this, 'sortByKeyFilter']),
|
||||
new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
|
||||
new \Twig_SimpleFilter('t', [$this, 'translate']),
|
||||
new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
|
||||
new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
|
||||
new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all functions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
|
||||
new \Twig_simpleFunction('authorize', [$this, 'authorize']),
|
||||
new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
|
||||
new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
|
||||
new \Twig_SimpleFunction('evaluate', [$this, 'evaluateFunc']),
|
||||
new \Twig_SimpleFunction('gist', [$this, 'gistFunc']),
|
||||
new \Twig_SimpleFunction('nonce_field', [$this, 'nonceFieldFunc']),
|
||||
new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']),
|
||||
new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']),
|
||||
new \Twig_SimpleFunction('string', [$this, 'stringFunc']),
|
||||
new \Twig_simpleFunction('t', [$this, 'translate']),
|
||||
new \Twig_simpleFunction('ta', [$this, 'translateArray']),
|
||||
new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
|
||||
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters field name by changing dot notation into array notation.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function fieldNameFilter($str)
|
||||
{
|
||||
$path = explode('.', $str);
|
||||
|
||||
return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Protects email address.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function safeEmailFilter($str)
|
||||
{
|
||||
$email = '';
|
||||
$str_len = strlen($str);
|
||||
for ($i = 0; $i < $str_len; $i++) {
|
||||
$email .= "&#" . ord($str[$i]) . ";";
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array in a random order.
|
||||
*
|
||||
* @param array $original
|
||||
* @param int $offset Can be used to return only slice of the array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function randomizeFilter($original, $offset = 0)
|
||||
{
|
||||
if (!is_array($original)) {
|
||||
return $original;
|
||||
}
|
||||
|
||||
if ($original instanceof \Traversable) {
|
||||
$original = iterator_to_array($original, false);
|
||||
}
|
||||
|
||||
$sorted = [];
|
||||
$random = array_slice($original, $offset);
|
||||
shuffle($random);
|
||||
|
||||
$sizeOf = sizeof($original);
|
||||
for ($x = 0; $x < $sizeOf; $x++) {
|
||||
if ($x < $offset) {
|
||||
$sorted[] = $original[$x];
|
||||
} else {
|
||||
$sorted[] = array_shift($random);
|
||||
}
|
||||
}
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modulus of an integer
|
||||
*
|
||||
* @param int $number
|
||||
* @param int $divider
|
||||
* @param array $items array of items to select from to return
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function modulusFilter($number, $divider, $items = null)
|
||||
{
|
||||
if (is_string($number)) {
|
||||
$number = strlen($number);
|
||||
}
|
||||
|
||||
$remainder = $number % $divider;
|
||||
|
||||
if (is_array($items)) {
|
||||
if (isset($items[$remainder])) {
|
||||
return $items[$remainder];
|
||||
} else {
|
||||
return $items[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $remainder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflector supports following notations:
|
||||
*
|
||||
* {{ 'person'|pluralize }} => people
|
||||
* {{ 'shoes'|singularize }} => shoe
|
||||
* {{ 'welcome page'|titleize }} => "Welcome Page"
|
||||
* {{ 'send_email'|camelize }} => SendEmail
|
||||
* {{ 'CamelCased'|underscorize }} => camel_cased
|
||||
* {{ 'Something Text'|hyphenize }} => something-text
|
||||
* {{ 'something_text_to_read'|humanize }} => "Something text to read"
|
||||
* {{ '181'|monthize }} => 6
|
||||
* {{ '10'|ordinalize }} => 10th
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $data
|
||||
* @param int $count
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function inflectorFilter($action, $data, $count = null)
|
||||
{
|
||||
$action = $action . 'ize';
|
||||
|
||||
$inflector = $this->grav['inflector'];
|
||||
|
||||
if (in_array(
|
||||
$action,
|
||||
['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize']
|
||||
)) {
|
||||
return $inflector->$action($data);
|
||||
} elseif (in_array($action, ['pluralize', 'singularize'])) {
|
||||
if ($count) {
|
||||
return $inflector->$action($data, $count);
|
||||
} else {
|
||||
return $inflector->$action($data);
|
||||
}
|
||||
} else {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return MD5 hash from the input.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function md5Filter($str)
|
||||
{
|
||||
return md5($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a collection by key
|
||||
*
|
||||
* @param array $input
|
||||
* @param string $filter
|
||||
* @param array|int $direction
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sortByKeyFilter(array $input, $filter, $direction = SORT_ASC)
|
||||
{
|
||||
$output = [];
|
||||
|
||||
if (!$input) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
foreach ($input as $key => $row) {
|
||||
$output[$key] = $row[$filter];
|
||||
}
|
||||
|
||||
array_multisort($output, $direction, $input);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return ksorted collection.
|
||||
*
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ksortFilter(array $array)
|
||||
{
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* determine if a string contains another
|
||||
*
|
||||
* @param String $haystack
|
||||
* @param String $needle
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function containsFilter($haystack, $needle)
|
||||
{
|
||||
return (strpos($haystack, $needle) !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* displays a facebook style 'time ago' formatted date/time
|
||||
*
|
||||
* @param $date
|
||||
* @param $long_strings
|
||||
* @param String
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function nicetimeFilter($date, $long_strings = true)
|
||||
{
|
||||
if (empty($date)) {
|
||||
return $this->grav['language']->translate('NICETIME.NO_DATE_PROVIDED', null, true);
|
||||
}
|
||||
|
||||
if ($long_strings) {
|
||||
$periods = [
|
||||
"NICETIME.SECOND",
|
||||
"NICETIME.MINUTE",
|
||||
"NICETIME.HOUR",
|
||||
"NICETIME.DAY",
|
||||
"NICETIME.WEEK",
|
||||
"NICETIME.MONTH",
|
||||
"NICETIME.YEAR",
|
||||
"NICETIME.DECADE"
|
||||
];
|
||||
} else {
|
||||
$periods = [
|
||||
"NICETIME.SEC",
|
||||
"NICETIME.MIN",
|
||||
"NICETIME.HR",
|
||||
"NICETIME.DAY",
|
||||
"NICETIME.WK",
|
||||
"NICETIME.MO",
|
||||
"NICETIME.YR",
|
||||
"NICETIME.DEC"
|
||||
];
|
||||
}
|
||||
|
||||
$lengths = ["60", "60", "24", "7", "4.35", "12", "10"];
|
||||
|
||||
$now = time();
|
||||
|
||||
// check if unix timestamp
|
||||
if ((string)(int)$date == $date) {
|
||||
$unix_date = $date;
|
||||
} else {
|
||||
$unix_date = strtotime($date);
|
||||
}
|
||||
|
||||
// check validity of date
|
||||
if (empty($unix_date)) {
|
||||
return $this->grav['language']->translate('NICETIME.BAD_DATE', null, true);
|
||||
}
|
||||
|
||||
// is it future date or past date
|
||||
if ($now > $unix_date) {
|
||||
$difference = $now - $unix_date;
|
||||
$tense = $this->grav['language']->translate('NICETIME.AGO', null, true);
|
||||
|
||||
} else {
|
||||
$difference = $unix_date - $now;
|
||||
$tense = $this->grav['language']->translate('NICETIME.FROM_NOW', null, true);
|
||||
}
|
||||
|
||||
for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) {
|
||||
$difference /= $lengths[$j];
|
||||
}
|
||||
|
||||
$difference = round($difference);
|
||||
|
||||
if ($difference != 1) {
|
||||
$periods[$j] .= '_PLURAL';
|
||||
}
|
||||
|
||||
if ($this->grav['language']->getTranslation($this->grav['language']->getLanguage(),
|
||||
$periods[$j] . '_MORE_THAN_TWO')
|
||||
) {
|
||||
if ($difference > 2) {
|
||||
$periods[$j] .= '_MORE_THAN_TWO';
|
||||
}
|
||||
}
|
||||
|
||||
$periods[$j] = $this->grav['language']->translate($periods[$j], null, true);
|
||||
|
||||
return "$difference $periods[$j] {$tense}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function absoluteUrlFilter($string)
|
||||
{
|
||||
$url = $this->grav['uri']->base();
|
||||
$string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string);
|
||||
|
||||
return $string;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function markdownFilter($string)
|
||||
{
|
||||
$page = $this->grav['page'];
|
||||
$defaults = $this->config->get('system.pages.markdown');
|
||||
|
||||
// Initialize the preferred variant of Parsedown
|
||||
if ($defaults['extra']) {
|
||||
$parsedown = new ParsedownExtra($page, $defaults);
|
||||
} else {
|
||||
$parsedown = new Parsedown($page, $defaults);
|
||||
}
|
||||
|
||||
$string = $parsedown->text($string);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function startsWithFilter($haystack, $needle)
|
||||
{
|
||||
return Utils::startsWith($haystack, $needle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function endsWithFilter($haystack, $needle)
|
||||
{
|
||||
return Utils::endsWith($haystack, $needle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param null $default
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function definedDefaultFilter($value, $default = null)
|
||||
{
|
||||
if (isset($value)) {
|
||||
return $value;
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param null $chars
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rtrimFilter($value, $chars = null)
|
||||
{
|
||||
return rtrim($value, $chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param null $chars
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ltrimFilter($value, $chars = null)
|
||||
{
|
||||
return ltrim($value, $chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function translate()
|
||||
{
|
||||
return $this->grav['language']->translate(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @param $index
|
||||
* @param null $lang
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function translateArray($key, $index, $lang = null)
|
||||
{
|
||||
return $this->grav['language']->translateArray($key, $index, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeat given string x times.
|
||||
*
|
||||
* @param string $input
|
||||
* @param int $multiplier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function repeatFunc($input, $multiplier)
|
||||
{
|
||||
return str_repeat($input, $multiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URL to the resource.
|
||||
*
|
||||
* @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
|
||||
*
|
||||
* @param string $input Resource to be located.
|
||||
* @param bool $domain True to include domain name.
|
||||
*
|
||||
* @return string|null Returns url to the resource or null if resource was not found.
|
||||
*/
|
||||
public function urlFunc($input, $domain = false)
|
||||
{
|
||||
if (!trim((string)$input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->grav['config']->get('system.absolute_urls', false)) {
|
||||
$domain = true;
|
||||
}
|
||||
|
||||
|
||||
if (strpos((string)$input, '://')) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
// Get relative path to the resource (or false if not found).
|
||||
$resource = $locator->findResource((string)$input, false);
|
||||
} else {
|
||||
$resource = (string)$input;
|
||||
}
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = $this->grav['uri'];
|
||||
|
||||
return $resource ? rtrim($uri->rootUrl($domain), '/') . '/' . $resource : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a string
|
||||
*
|
||||
* @example {{ evaluate('grav.language.getLanguage') }}
|
||||
*
|
||||
* @param string $input String to be evaluated
|
||||
*
|
||||
* @return string Returns the evaluated string
|
||||
*/
|
||||
public function evaluateFunc($input)
|
||||
{
|
||||
return $this->grav['twig']->processString("{{ $input }}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on Twig_Extension_Debug / twig_var_dump
|
||||
* (c) 2011 Fabien Potencier
|
||||
*
|
||||
* @param \Twig_Environment $env
|
||||
* @param $context
|
||||
*/
|
||||
public function dump(\Twig_Environment $env, $context)
|
||||
{
|
||||
if (!$env->isDebug() || !$this->debugger) {
|
||||
return;
|
||||
}
|
||||
|
||||
$count = func_num_args();
|
||||
if (2 === $count) {
|
||||
$data = [];
|
||||
foreach ($context as $key => $value) {
|
||||
if (is_object($value)) {
|
||||
if (method_exists($value, 'toArray')) {
|
||||
$data[$key] = $value->toArray();
|
||||
} else {
|
||||
$data[$key] = "Object (" . get_class($value) . ")";
|
||||
}
|
||||
} else {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
}
|
||||
$this->debugger->addMessage($data, 'debug');
|
||||
} else {
|
||||
for ($i = 2; $i < $count; $i++) {
|
||||
$this->debugger->addMessage(func_get_arg($i), 'debug');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a Gist
|
||||
*
|
||||
* @param string $id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function gistFunc($id)
|
||||
{
|
||||
return '<script src="https://gist.github.com/' . $id . '.js"></script>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string
|
||||
*
|
||||
* @param int $count
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function randomStringFunc($count = 5)
|
||||
{
|
||||
return Utils::generateRandomString($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad a string to a certain length with another string
|
||||
*
|
||||
* @param $input
|
||||
* @param $pad_length
|
||||
* @param string $pad_string
|
||||
* @param int $pad_type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function padFilter($input, $pad_length, $pad_string = " ", $pad_type = STR_PAD_RIGHT)
|
||||
{
|
||||
return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cast a value to array
|
||||
*
|
||||
* @param $value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function arrayFunc($value)
|
||||
{
|
||||
return (array)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string from a value. If the value is array, return it json encoded
|
||||
*
|
||||
* @param $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function stringFunc($value)
|
||||
{
|
||||
if (is_array($value)) { //format the array as a string
|
||||
return json_encode($value);
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function translateFunc()
|
||||
{
|
||||
return $this->grav['language']->translate(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize an action. Returns true if the user is logged in and has the right to execute $action.
|
||||
*
|
||||
* @param string $action
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize($action)
|
||||
{
|
||||
if (!$this->grav['user']->authenticated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$action = (array)$action;
|
||||
|
||||
foreach ($action as $a) {
|
||||
if ($this->grav['user']->authorize($a)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action.
|
||||
*
|
||||
* For maximum protection, ensure that the string representing the action is as specific as possible
|
||||
*
|
||||
* @param string $action the action
|
||||
* @param string $nonceParamName a custom nonce param name
|
||||
*
|
||||
* @return string the nonce input field
|
||||
*/
|
||||
public function nonceFieldFunc($action, $nonceParamName = 'nonce')
|
||||
{
|
||||
$string = '<input type="hidden" id="' . $nonceParamName . '" name="' . $nonceParamName . '" value="' . Utils::getNonce($action) . '" />';
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace Grav\Common\Twig;
|
||||
|
||||
use Grav\Common\GravTrait;
|
||||
|
||||
/**
|
||||
* A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
|
||||
*/
|
||||
trait WriteCacheFileTrait
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected static $umask;
|
||||
|
||||
/**
|
||||
* This exists so template cache files use the same
|
||||
* group between apache and cli
|
||||
*
|
||||
* @param $file
|
||||
* @param $content
|
||||
*/
|
||||
protected function writeCacheFile($file, $content)
|
||||
{
|
||||
if (empty($file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset(self::$umask)) {
|
||||
self::$umask = self::getGrav()['config']->get('system.twig.umask_fix', false);
|
||||
}
|
||||
|
||||
if (self::$umask) {
|
||||
if (!is_dir(dirname($file))) {
|
||||
$old = umask(0002);
|
||||
mkdir(dirname($file), 0777, true);
|
||||
umask($old);
|
||||
}
|
||||
parent::writeCacheFile($file, $content);
|
||||
chmod($file, 0775);
|
||||
} else {
|
||||
parent::writeCacheFile($file, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace Grav\Common\User;
|
||||
|
||||
/**
|
||||
* User authentication
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class Authentication
|
||||
{
|
||||
/**
|
||||
* Create password hash from plaintext password.
|
||||
*
|
||||
* @param string $password Plaintext password.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @return string|bool
|
||||
*/
|
||||
public static function create($password)
|
||||
{
|
||||
if (!$password) {
|
||||
throw new \RuntimeException('Password hashing failed: no password provided.');
|
||||
}
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
if (!$hash) {
|
||||
throw new \RuntimeException('Password hashing failed: internal error.');
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a password matches a hash.
|
||||
*
|
||||
* @param string $password Plaintext password.
|
||||
* @param string $hash Hash to verify against.
|
||||
*
|
||||
* @return int Returns 0 if the check fails, 1 if password matches, 2 if hash needs to be updated.
|
||||
*/
|
||||
public static function verify($password, $hash)
|
||||
{
|
||||
// Fail if hash doesn't match
|
||||
if (!$password || !$hash || !password_verify($password, $hash)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Otherwise check if hash needs an update.
|
||||
return password_needs_rehash($hash, PASSWORD_DEFAULT) ? 2 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
namespace Grav\Common\User;
|
||||
|
||||
use Grav\Common\Data\Blueprints;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* Group object
|
||||
*
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class Group extends Data
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* Get the groups list
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function groups()
|
||||
{
|
||||
$groups = self::getGrav()['config']->get('groups');
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a group exists
|
||||
*
|
||||
* @param string $groupname
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public static function groupExists($groupname)
|
||||
{
|
||||
return isset(self::groups()[$groupname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group by name
|
||||
*
|
||||
* @param string $groupname
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public static function load($groupname)
|
||||
{
|
||||
if (self::groupExists($groupname)) {
|
||||
$content = self::groups()[$groupname];
|
||||
} else {
|
||||
$content = [];
|
||||
}
|
||||
|
||||
$blueprints = new Blueprints('blueprints://');
|
||||
$blueprint = $blueprints->get('user/group');
|
||||
if (!isset($content['groupname'])) {
|
||||
$content['groupname'] = $groupname;
|
||||
}
|
||||
$group = new Group($content, $blueprint);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a group
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$blueprints = new Blueprints('blueprints://');
|
||||
$blueprint = $blueprints->get('user/group');
|
||||
|
||||
$fields = $blueprint->fields();
|
||||
|
||||
self::getGrav()['config']->set("groups.$this->groupname", []);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ($field['type'] == 'text') {
|
||||
$value = $field['name'];
|
||||
if (isset($this->items[$value])) {
|
||||
self::getGrav()['config']->set("groups.$this->groupname.$value", $this->items[$value]);
|
||||
}
|
||||
}
|
||||
if ($field['type'] == 'array') {
|
||||
$value = $field['name'];
|
||||
$arrayValues = Utils::resolve($this->items, $field['name']);
|
||||
|
||||
if ($arrayValues) {
|
||||
foreach ($arrayValues as $arrayIndex => $arrayValue) {
|
||||
self::getGrav()['config']->set("groups.$this->groupname.$value.$arrayIndex", $arrayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$type = 'groups';
|
||||
$blueprints = $this->blueprints("config/{$type}");
|
||||
$obj = new Data(self::getGrav()['config']->get($type), $blueprints);
|
||||
$file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource("config://{$type}.yaml"));
|
||||
$obj->file($file);
|
||||
$obj->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a group
|
||||
*
|
||||
* @param string $groupname
|
||||
*
|
||||
* @return bool True if the action was performed
|
||||
*/
|
||||
public static function remove($groupname)
|
||||
{
|
||||
$blueprints = new Blueprints('blueprints://');
|
||||
$blueprint = $blueprints->get('user/group');
|
||||
|
||||
$groups = self::getGrav()['config']->get("groups");
|
||||
unset($groups[$groupname]);
|
||||
self::getGrav()['config']->set("groups", $groups);
|
||||
|
||||
$type = 'groups';
|
||||
$obj = new Data(self::getGrav()['config']->get($type), $blueprint);
|
||||
$file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource("config://{$type}.yaml"));
|
||||
$obj->file($file);
|
||||
$obj->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
namespace Grav\Common\User;
|
||||
|
||||
use Grav\Common\Data\Blueprints;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* User object
|
||||
*
|
||||
* @property mixed authenticated
|
||||
* @property mixed password
|
||||
* @property bool|string hashed_password
|
||||
* @author RocketTheme
|
||||
* @license MIT
|
||||
*/
|
||||
class User extends Data
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
/**
|
||||
* Load user account.
|
||||
*
|
||||
* Always creates user object. To check if user exists, use $this->exists().
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public static function load($username)
|
||||
{
|
||||
$locator = self::getGrav()['locator'];
|
||||
|
||||
// force lowercase of username
|
||||
$username = strtolower($username);
|
||||
|
||||
$blueprints = new Blueprints('blueprints://');
|
||||
$blueprint = $blueprints->get('user/account');
|
||||
$file_path = $locator->findResource('account://' . $username . YAML_EXT);
|
||||
$file = CompiledYamlFile::instance($file_path);
|
||||
$content = $file->content();
|
||||
if (!isset($content['username'])) {
|
||||
$content['username'] = $username;
|
||||
}
|
||||
if (!isset($content['state'])) {
|
||||
$content['state'] = 'enabled';
|
||||
}
|
||||
$user = new User($content, $blueprint);
|
||||
$user->file($file);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user account.
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return bool True if the action was performed
|
||||
*/
|
||||
public static function remove($username)
|
||||
{
|
||||
$file_path = self::getGrav()['locator']->findResource('account://' . $username . YAML_EXT);
|
||||
if (file_exists($file_path) && unlink($file_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user.
|
||||
*
|
||||
* If user password needs to be updated, new information will be saved.
|
||||
*
|
||||
* @param string $password Plaintext password.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticate($password)
|
||||
{
|
||||
$save = false;
|
||||
|
||||
// Plain-text is still stored
|
||||
if ($this->password) {
|
||||
if ($password !== $this->password) {
|
||||
// Plain-text passwords do not match, we know we should fail but execute
|
||||
// verify to protect us from timing attacks and return false regardless of
|
||||
// the result
|
||||
Authentication::verify(
|
||||
$password,
|
||||
self::getGrav()['config']->get('system.security.default_hash')
|
||||
);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
// Plain-text does match, we can update the hash and proceed
|
||||
$save = true;
|
||||
|
||||
$this->hashed_password = Authentication::create($this->password);
|
||||
unset($this->password);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$result = Authentication::verify($password, $this->hashed_password);
|
||||
|
||||
// Password needs to be updated, save the file.
|
||||
if ($result == 2) {
|
||||
$save = true;
|
||||
$this->hashed_password = Authentication::create($password);
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user without the username
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$file = $this->file();
|
||||
if ($file) {
|
||||
// if plain text password, hash it and remove plain text
|
||||
if ($this->password) {
|
||||
$this->hashed_password = Authentication::create($this->password);
|
||||
unset($this->password);
|
||||
}
|
||||
|
||||
$username = $this->get('username');
|
||||
unset($this->username);
|
||||
$file->save($this->items);
|
||||
$this->set('username', $username);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
*
|
||||
* @param string $action
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize($action)
|
||||
{
|
||||
if (empty($this->items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->state) && $this->state !== 'enabled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$return = false;
|
||||
|
||||
//Check group access level
|
||||
$groups = $this->get('groups');
|
||||
if ($groups) {
|
||||
foreach ((array)$groups as $group) {
|
||||
$permission = self::getGrav()['config']->get("groups.{$group}.access.{$action}");
|
||||
$return = Utils::isPositive($permission);
|
||||
if ($return === true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check user access level
|
||||
if ($this->get('access')) {
|
||||
if (Utils::resolve($this->get('access'), $action) !== null) {
|
||||
$permission = $this->get("access.{$action}");
|
||||
$return = Utils::isPositive($permission);
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
* Ensures backwards compatibility
|
||||
*
|
||||
* @param string $action
|
||||
*
|
||||
* @deprecated use authorize()
|
||||
* @return bool
|
||||
*/
|
||||
public function authorise($action)
|
||||
{
|
||||
return $this->authorize($action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
<?php
|
||||
namespace Grav\Common;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Grav\Common\Helpers\Truncator;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* Misc utilities.
|
||||
*
|
||||
* @package Grav\Common
|
||||
*/
|
||||
abstract class Utils
|
||||
{
|
||||
use GravTrait;
|
||||
|
||||
protected static $nonces = [];
|
||||
|
||||
/**
|
||||
* Check if the $haystack string starts with the substring $needle
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function startsWith($haystack, $needle)
|
||||
{
|
||||
if (is_array($needle)) {
|
||||
$status = false;
|
||||
foreach ($needle as $each_needle) {
|
||||
$status = $status || ($each_needle === '' || strpos($haystack, $each_needle) === 0);
|
||||
if ($status) {
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $needle === '' || strpos($haystack, $needle) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the $haystack string ends with the substring $needle
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function endsWith($haystack, $needle)
|
||||
{
|
||||
if (is_array($needle)) {
|
||||
$status = false;
|
||||
foreach ($needle as $each_needle) {
|
||||
$status = $status || ($each_needle === '' || substr($haystack, -strlen($each_needle)) === $each_needle);
|
||||
if ($status) {
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $needle === '' || substr($haystack, -strlen($needle)) === $needle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the $haystack string contains the substring $needle
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function contains($haystack, $needle)
|
||||
{
|
||||
return $needle === '' || strpos($haystack, $needle) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the substring of a string up to a specified needle. if not found, return the whole haytack
|
||||
*
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function substrToString($haystack, $needle)
|
||||
{
|
||||
if (static::contains($haystack, $needle)) {
|
||||
return substr($haystack, 0, strpos($haystack, $needle));
|
||||
}
|
||||
|
||||
return $haystack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two objects into one.
|
||||
*
|
||||
* @param object $obj1
|
||||
* @param object $obj2
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public static function mergeObjects($obj1, $obj2)
|
||||
{
|
||||
return (object)array_merge((array)$obj1, (array)$obj2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Grav date formats allowed
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function dateFormats()
|
||||
{
|
||||
$now = new DateTime();
|
||||
|
||||
$date_formats = [
|
||||
'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
|
||||
'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
|
||||
'm/d/Y h:i a' => 'm/d/Y h:i (e.g. '.$now->format('m/d/Y h:i a').')',
|
||||
'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
|
||||
'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
|
||||
];
|
||||
$default_format = self::getGrav()['config']->get('system.pages.dateformat.default');
|
||||
if ($default_format) {
|
||||
$date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
|
||||
}
|
||||
|
||||
return $date_formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text by number of characters but can cut off words.
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $limit Max number of characters.
|
||||
* @param bool $up_to_break truncate up to breakpoint after char count
|
||||
* @param string $break Break point.
|
||||
* @param string $pad Appended padding to the end of the string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function truncate($string, $limit = 150, $up_to_break = false, $break = " ", $pad = "…")
|
||||
{
|
||||
// return with no change if string is shorter than $limit
|
||||
if (mb_strlen($string) <= $limit) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
// is $break present between $limit and the end of the string?
|
||||
if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
|
||||
if ($breakpoint < mb_strlen($string) - 1) {
|
||||
$string = mb_substr($string, 0, $breakpoint) . $break;
|
||||
}
|
||||
} else {
|
||||
$string = mb_substr($string, 0, $limit) . $pad;
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text by number of characters in a "word-safe" manor.
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $limit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function safeTruncate($string, $limit = 150)
|
||||
{
|
||||
return static::truncate($string, $limit, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Truncate HTML by number of characters. not "word-safe"!
|
||||
*
|
||||
* @param string $text
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function truncateHtml($text, $length = 100)
|
||||
{
|
||||
return Truncator::truncate($text, $length, ['length_in_chars' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate HTML by number of characters in a "word-safe" manor.
|
||||
*
|
||||
* @param string $text
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function safeTruncateHtml($text, $length = 100)
|
||||
{
|
||||
return Truncator::truncate($text, $length, ['length_in_chars' => true, 'word_safe' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string of a given length
|
||||
*
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generateRandomString($length = 5)
|
||||
{
|
||||
return substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the ability to download a file to the browser
|
||||
*
|
||||
* @param string $file the full path to the file to be downloaded
|
||||
* @param bool $force_download as opposed to letting browser choose if to download or render
|
||||
*/
|
||||
public static function download($file, $force_download = true)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
// fire download event
|
||||
self::getGrav()->fireEvent('onBeforeDownload', new Event(['file' => $file]));
|
||||
|
||||
$file_parts = pathinfo($file);
|
||||
$filesize = filesize($file);
|
||||
|
||||
// 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) {
|
||||
}
|
||||
|
||||
ignore_user_abort(false);
|
||||
|
||||
if ($force_download) {
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename=' . $file_parts['basename']);
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||
header('Pragma: public');
|
||||
} else {
|
||||
header("Content-Type: " . Utils::getMimeType($file_parts['extension']));
|
||||
}
|
||||
header('Content-Length: ' . $filesize);
|
||||
|
||||
// 8kb chunks for now
|
||||
$chunk = 8 * 1024;
|
||||
|
||||
$fh = fopen($file, "rb");
|
||||
|
||||
if ($fh === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Repeat reading until EOF
|
||||
while (!feof($fh)) {
|
||||
echo fread($fh, $chunk);
|
||||
|
||||
ob_flush(); // flush output
|
||||
flush();
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mimetype based on filename
|
||||
*
|
||||
* @param string $extension Extension of file (eg "txt")
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getMimeType($extension)
|
||||
{
|
||||
$extension = strtolower($extension);
|
||||
$config = self::getGrav()['config']->get('media');
|
||||
|
||||
if (isset($config[$extension])) {
|
||||
return $config[$extension]['mime'];
|
||||
}
|
||||
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path by processing relative `.` and `..` syntax and merging path
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function normalizePath($path)
|
||||
{
|
||||
$root = ($path[0] === '/') ? '/' : '';
|
||||
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
$ret = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (($segment == '.') || empty($segment)) {
|
||||
continue;
|
||||
}
|
||||
if ($segment == '..') {
|
||||
array_pop($ret);
|
||||
} else {
|
||||
array_push($ret, $segment);
|
||||
}
|
||||
}
|
||||
|
||||
return $root . implode('/', $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a function is disabled in the PHP settings
|
||||
*
|
||||
* @param string $function the name of the function to check
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isFunctionDisabled($function)
|
||||
{
|
||||
return in_array($function, explode(',', ini_get('disable_functions')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted timezones list
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function timezones()
|
||||
{
|
||||
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
|
||||
$offsets = [];
|
||||
$testDate = new \DateTime;
|
||||
|
||||
foreach ($timezones as $zone) {
|
||||
$tz = new \DateTimeZone($zone);
|
||||
$offsets[$zone] = $tz->getOffset($testDate);
|
||||
}
|
||||
|
||||
asort($offsets);
|
||||
|
||||
$timezone_list = [];
|
||||
foreach ($offsets as $timezone => $offset) {
|
||||
$offset_prefix = $offset < 0 ? '-' : '+';
|
||||
$offset_formatted = gmdate('H:i', abs($offset));
|
||||
|
||||
$pretty_offset = "UTC${offset_prefix}${offset_formatted}";
|
||||
|
||||
$timezone_list[$timezone] = "(${pretty_offset}) $timezone";
|
||||
}
|
||||
|
||||
return $timezone_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively filter an array, filtering values by processing them through the $fn function argument
|
||||
*
|
||||
* @param array $source the Array to filter
|
||||
* @param callable $fn the function to pass through each array item
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function arrayFilterRecursive(Array $source, $fn)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($source as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$result[$key] = static::arrayFilterRecursive($value, $fn);
|
||||
continue;
|
||||
}
|
||||
if ($fn($key, $value)) {
|
||||
$result[$key] = $value; // KEEP
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed path contains the language code prefix
|
||||
*
|
||||
* @param string $string The path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function pathPrefixedByLangCode($string)
|
||||
{
|
||||
if (strlen($string) <= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$languages_enabled = self::getGrav()['config']->get('system.languages.supported', []);
|
||||
|
||||
if ($string[0] == '/' && $string[3] == '/' && in_array(substr($string, 1, 2), $languages_enabled)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of a date
|
||||
*
|
||||
* @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a
|
||||
* strtotime argument
|
||||
*
|
||||
* @return int the timestamp
|
||||
*/
|
||||
public static function date2timestamp($date)
|
||||
{
|
||||
$config = self::getGrav()['config'];
|
||||
$default_dateformat = $config->get('system.pages.dateformat.default');
|
||||
|
||||
// try to use DateTime and default format
|
||||
if ($default_dateformat) {
|
||||
$datetime = DateTime::createFromFormat($default_dateformat, $date);
|
||||
} else {
|
||||
$datetime = new DateTime($date);
|
||||
}
|
||||
|
||||
// fallback to strtotime if DateTime approach failed
|
||||
if ($datetime !== false) {
|
||||
return $datetime->getTimestamp();
|
||||
} else {
|
||||
return strtotime($date);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of an array element using dot notation
|
||||
*
|
||||
* @param array $array the Array to check
|
||||
* @param string $path the dot notation path to check
|
||||
* @param mixed $default a value to be returned if $path is not found in $array
|
||||
*
|
||||
* @return mixed the value found
|
||||
*/
|
||||
public static function resolve(array $array, $path, $default = null)
|
||||
{
|
||||
$current = $array;
|
||||
$p = strtok($path, '.');
|
||||
|
||||
while ($p !== false) {
|
||||
if (!isset($current[$p])) {
|
||||
return $default;
|
||||
}
|
||||
$current = $current[$p];
|
||||
$p = strtok('.');
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is positive
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isPositive($value)
|
||||
{
|
||||
return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a nonce string to be hashed. Called by self::getNonce()
|
||||
* We removed the IP portion in this version because it causes too many inconsistencies
|
||||
* with reverse proxy setups.
|
||||
*
|
||||
* @param string $action
|
||||
* @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours)
|
||||
*
|
||||
* @return string the nonce string
|
||||
*/
|
||||
private static function generateNonceString($action, $plusOneTick = false)
|
||||
{
|
||||
$username = '';
|
||||
if (isset(self::getGrav()['user'])) {
|
||||
$user = self::getGrav()['user'];
|
||||
$username = $user->username;
|
||||
}
|
||||
|
||||
$token = session_id();
|
||||
$i = self::nonceTick();
|
||||
|
||||
if ($plusOneTick) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . self::getGrav()['config']->get('security.salt'));
|
||||
}
|
||||
|
||||
//Added in version 1.0.8 to ensure that existing nonces are not broken.
|
||||
private static function generateNonceStringOldStyle($action, $plusOneTick = false)
|
||||
{
|
||||
if (isset(self::getGrav()['user'])) {
|
||||
$user = self::getGrav()['user'];
|
||||
$username = $user->username;
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
$username .= $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
} else {
|
||||
$username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
|
||||
}
|
||||
$token = session_id();
|
||||
$i = self::nonceTick();
|
||||
if ($plusOneTick) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . self::getGrav()['config']->get('security.salt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time-dependent variable for nonce creation.
|
||||
*
|
||||
* Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way
|
||||
* to ensure nonces issued near the end of the day do not expire in that small amount of time
|
||||
*
|
||||
* @return int the time part of the nonce. Changes once every 24 hours
|
||||
*/
|
||||
private static function nonceTick()
|
||||
{
|
||||
$secondsInHalfADay = 60 * 60 * 12;
|
||||
|
||||
return (int)ceil(time() / ($secondsInHalfADay));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
|
||||
* action is the same for 12 hours.
|
||||
*
|
||||
* @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
|
||||
* @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours)
|
||||
*
|
||||
* @return string the nonce
|
||||
*/
|
||||
public static function getNonce($action, $plusOneTick = false)
|
||||
{
|
||||
// Don't regenerate this again if not needed
|
||||
if (isset(static::$nonces[$action])) {
|
||||
return static::$nonces[$action];
|
||||
}
|
||||
$nonce = md5(self::generateNonceString($action, $plusOneTick));
|
||||
static::$nonces[$action] = $nonce;
|
||||
|
||||
return static::$nonces[$action];
|
||||
}
|
||||
|
||||
//Added in version 1.0.8 to ensure that existing nonces are not broken.
|
||||
public static function getNonceOldStyle($action, $plusOneTick = false)
|
||||
{
|
||||
// Don't regenerate this again if not needed
|
||||
if (isset(static::$nonces[$action])) {
|
||||
return static::$nonces[$action];
|
||||
}
|
||||
$nonce = md5(self::generateNonceStringOldStyle($action, $plusOneTick));
|
||||
static::$nonces[$action] = $nonce;
|
||||
|
||||
return static::$nonces[$action];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the passed nonce for the give action
|
||||
*
|
||||
* @param string $nonce the nonce to verify
|
||||
* @param string $action the action to verify the nonce to
|
||||
*
|
||||
* @return boolean verified or not
|
||||
*/
|
||||
public static function verifyNonce($nonce, $action)
|
||||
{
|
||||
//Safety check for multiple nonces
|
||||
if (is_array($nonce)) {
|
||||
$nonce = array_shift($nonce);
|
||||
}
|
||||
|
||||
//Nonce generated 0-12 hours ago
|
||||
if ($nonce == self::getNonce($action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Nonce generated 12-24 hours ago
|
||||
$plusOneTick = true;
|
||||
if ($nonce == self::getNonce($action, $plusOneTick)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//Added in version 1.0.8 to ensure that existing nonces are not broken.
|
||||
//Nonce generated 0-12 hours ago
|
||||
if ($nonce == self::getNonceOldStyle($action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Nonce generated 12-24 hours ago
|
||||
$plusOneTick = true;
|
||||
if ($nonce == self::getNonceOldStyle($action, $plusOneTick)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Invalid nonce
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Common\Backup\ZipBackup;
|
||||
use Grav\Console\ConsoleCommand;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
/**
|
||||
* Class BackupCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class BackupCommand extends ConsoleCommand
|
||||
{
|
||||
protected $source;
|
||||
protected $progress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName("backup")
|
||||
->addArgument(
|
||||
'destination',
|
||||
InputArgument::OPTIONAL,
|
||||
'Where to store the backup (/backup is default)'
|
||||
|
||||
)
|
||||
->setDescription("Creates a backup of the Grav instance")
|
||||
->setHelp('The <info>backup</info> creates a zipped backup. Optionally can be saved in a different destination.');
|
||||
|
||||
$this->source = getcwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function serve()
|
||||
{
|
||||
$this->progress = new ProgressBar($this->output);
|
||||
$this->progress->setFormat('Archiving <cyan>%current%</cyan> files [<green>%bar%</green>] %elapsed:6s% %memory:6s%');
|
||||
|
||||
self::getGrav()['config']->init();
|
||||
|
||||
$destination = ($this->input->getArgument('destination')) ? $this->input->getArgument('destination') : null;
|
||||
$log = JsonFile::instance(self::getGrav()['locator']->findResource("log://backup.log", true, true));
|
||||
$backup = ZipBackup::backup($destination, [$this, 'output']);
|
||||
|
||||
$log->content([
|
||||
'time' => time(),
|
||||
'location' => $backup
|
||||
]);
|
||||
$log->save();
|
||||
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $args
|
||||
*/
|
||||
public function output($args)
|
||||
{
|
||||
switch ($args['type']) {
|
||||
case 'message':
|
||||
$this->output->writeln($args['message']);
|
||||
break;
|
||||
case 'progress':
|
||||
if ($args['complete']) {
|
||||
$this->progress->finish();
|
||||
} else {
|
||||
$this->progress->advance();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
|
||||
/**
|
||||
* Class CleanCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class CleanCommand extends Command
|
||||
{
|
||||
/* @var InputInterface $output */
|
||||
protected $input;
|
||||
|
||||
/* @var OutputInterface $output */
|
||||
protected $output;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $paths_to_remove = [
|
||||
'codeception.yml',
|
||||
'tests/',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/.travis.yml',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/build.xml',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/composer.json',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/create_pear_package.php',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/package.xml.tpl',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/.gitattributes',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/.gitignore',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/README.git',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/tests',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/test-suite',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/notes',
|
||||
'user/plugins/email/vendor/swiftmailer/swiftmailer/doc',
|
||||
'user/themes/antimatter/.sass-cache',
|
||||
'vendor/donatj/phpuseragentparser/.git',
|
||||
'vendor/donatj/phpuseragentparser/.gitignore',
|
||||
'vendor/donatj/phpuseragentparser/.travis.yml',
|
||||
'vendor/donatj/phpuseragentparser/composer.json',
|
||||
'vendor/donatj/phpuseragentparser/phpunit.xml.dist',
|
||||
'vendor/donatj/phpuseragentparser/Tests',
|
||||
'vendor/donatj/phpuseragentparser/Tools',
|
||||
'vendor/doctrine/cache/.travis.yml',
|
||||
'vendor/doctrine/cache/build.properties',
|
||||
'vendor/doctrine/cache/build.xml',
|
||||
'vendor/doctrine/cache/composer.json',
|
||||
'vendor/doctrine/cache/phpunit.xml.dist',
|
||||
'vendor/doctrine/cache/.coveralls.yml',
|
||||
'vendor/doctrine/cache/.gitignore',
|
||||
'vendor/doctrine/cache/.git',
|
||||
'vendor/doctrine/cache/tests',
|
||||
'vendor/erusev/parsedown/composer.json',
|
||||
'vendor/erusev/parsedown/phpunit.xml.dist',
|
||||
'vendor/erusev/parsedown/.travis.yml',
|
||||
'vendor/erusev/parsedown/.git',
|
||||
'vendor/erusev/parsedown/test',
|
||||
'vendor/erusev/parsedown-extra/composer.json',
|
||||
'vendor/erusev/parsedown-extra/phpunit.xml.dist',
|
||||
'vendor/erusev/parsedown-extra/.travis.yml',
|
||||
'vendor/erusev/parsedown-extra/.git',
|
||||
'vendor/erusev/parsedown-extra/test',
|
||||
'vendor/filp/whoops/composer.json',
|
||||
'vendor/filp/whoops/docs',
|
||||
'vendor/filp/whoops/examples',
|
||||
'vendor/filp/whoops/tests',
|
||||
'vendor/filp/whoops/.git',
|
||||
'vendor/filp/whoops/.gitignore',
|
||||
'vendor/filp/whoops/.scrutinizer.yml',
|
||||
'vendor/filp/whoops/.travis.yml',
|
||||
'vendor/filp/whoops/phpunit.xml.dist',
|
||||
'vendor/gregwar/image/Gregwar/Image/composer.json',
|
||||
'vendor/gregwar/image/Gregwar/Image/phpunit.xml',
|
||||
'vendor/gregwar/image/Gregwar/Image/.gitignore',
|
||||
'vendor/gregwar/image/Gregwar/Image/.git',
|
||||
'vendor/gregwar/image/Gregwar/Image/doc',
|
||||
'vendor/gregwar/image/Gregwar/Image/demo',
|
||||
'vendor/gregwar/image/Gregwar/Image/tests',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/composer.json',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/phpunit.xml',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/.gitignore',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/.git',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/demo',
|
||||
'vendor/gregwar/cache/Gregwar/Cache/tests',
|
||||
'vendor/ircmaxell/password-compat/composer.json',
|
||||
'vendor/ircmaxell/password-compat/phpunit.xml.dist',
|
||||
'vendor/ircmaxell/password-compat/version-test.php',
|
||||
'vendor/ircmaxell/password-compat/.travis.yml',
|
||||
'vendor/ircmaxell/password-compat/test',
|
||||
'vendor/maximebf/debugbar/bower.json',
|
||||
'vendor/maximebf/debugbar/composer.json',
|
||||
'vendor/maximebf/debugbar/.bowerrc',
|
||||
'vendor/maximebf/debugbar/src/DebugBar/Resources/vendor',
|
||||
'vendor/maximebf/debugbar/demo',
|
||||
'vendor/maximebf/debugbar/docs',
|
||||
'vendor/maximebf/debugbar/tests',
|
||||
'vendor/maximebf/debugbar/phpunit.xml.dist',
|
||||
'vendor/monolog/monolog/composer.json',
|
||||
'vendor/monolog/monolog/doc',
|
||||
'vendor/monolog/monolog/phpunit.xml.dist',
|
||||
'vendor/monolog/monolog/tests',
|
||||
'vendor/mrclay/minify/.editorconfig',
|
||||
'vendor/mrclay/minify/.git',
|
||||
'vendor/mrclay/minify/.gitignore',
|
||||
'vendor/mrclay/minify/composer.json',
|
||||
'vendor/mrclay/minify/min_extras',
|
||||
'vendor/mrclay/minify/min_unit_tests',
|
||||
'vendor/mrclay/minify/min/.htaccess',
|
||||
'vendor/mrclay/minify/min/builder',
|
||||
'vendor/mrclay/minify/min/config-test.php',
|
||||
'vendor/mrclay/minify/min/config.php',
|
||||
'vendor/mrclay/minify/min/groupsConfig.php',
|
||||
'vendor/mrclay/minify/min/index.php',
|
||||
'vendor/mrclay/minify/min/quick-test.css',
|
||||
'vendor/mrclay/minify/min/quick-test.js',
|
||||
'vendor/mrclay/minify/min/utils.php',
|
||||
'vendor/pimple/pimple/.gitignore',
|
||||
'vendor/pimple/pimple/.travis.yml',
|
||||
'vendor/pimple/pimple/composer.json',
|
||||
'vendor/pimple/pimple/ext',
|
||||
'vendor/pimple/pimple/phpunit.xml.dist',
|
||||
'vendor/pimple/pimple/src/Pimple/Tests',
|
||||
'vendor/psr/log/composer.json',
|
||||
'vendor/psr/log/.gitignore',
|
||||
'vendor/rockettheme/toolbox/.git',
|
||||
'vendor/rockettheme/toolbox/.gitignore',
|
||||
'vendor/rockettheme/toolbox/.scrutinizer.yml',
|
||||
'vendor/rockettheme/toolbox/.travis.yml',
|
||||
'vendor/rockettheme/toolbox/composer.json',
|
||||
'vendor/rockettheme/toolbox/phpunit.xml',
|
||||
'vendor/symfony/console/composer.json',
|
||||
'vendor/symfony/console/phpunit.xml.dist',
|
||||
'vendor/symfony/console/.gitignore',
|
||||
'vendor/symfony/console/.git',
|
||||
'vendor/symfony/console/Tester',
|
||||
'vendor/symfony/console/Tests',
|
||||
'vendor/symfony/event-dispatcher/.git',
|
||||
'vendor/symfony/event-dispatcher/.gitignore',
|
||||
'vendor/symfony/event-dispatcher/composer.json',
|
||||
'vendor/symfony/event-dispatcher/phpunit.xml.dist',
|
||||
'vendor/symfony/event-dispatcher/Tests',
|
||||
'vendor/symfony/polyfill-iconv/.git',
|
||||
'vendor/symfony/polyfill-iconv/.gitignore',
|
||||
'vendor/symfony/polyfill-iconv/composer.json',
|
||||
'vendor/symfony/polyfill-mbstring/.git',
|
||||
'vendor/symfony/polyfill-mbstring/.gitignore',
|
||||
'vendor/symfony/polyfill-mbstring/composer.json',
|
||||
'vendor/symfony/var-dumper/.git',
|
||||
'vendor/symfony/var-dumper/.gitignore',
|
||||
'vendor/symfony/var-dumper/composer.json',
|
||||
'vendor/symfony/var-dumper/phpunit.xml.dist',
|
||||
'vendor/symfony/var-dumper/Test',
|
||||
'vendor/symfony/var-dumper/Tests',
|
||||
'vendor/symfony/yaml/composer.json',
|
||||
'vendor/symfony/yaml/phpunit.xml.dist',
|
||||
'vendor/symfony/yaml/.gitignore',
|
||||
'vendor/symfony/yaml/.git',
|
||||
'vendor/symfony/yaml/Tests',
|
||||
'vendor/twig/twig/.editorconfig',
|
||||
'vendor/twig/twig/.travis.yml',
|
||||
'vendor/twig/twig/.gitignore',
|
||||
'vendor/twig/twig/.git',
|
||||
'vendor/twig/twig/composer.json',
|
||||
'vendor/twig/twig/phpunit.xml.dist',
|
||||
'vendor/twig/twig/doc',
|
||||
'vendor/twig/twig/ext',
|
||||
'vendor/twig/twig/test',
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName("clean")
|
||||
->setDescription("Handles cleaning chores for Grav distribution")
|
||||
->setHelp('The <info>clean</info> clean extraneous folders and data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->setupConsole($input, $output);
|
||||
|
||||
$this->cleanPaths();
|
||||
}
|
||||
|
||||
private function cleanPaths()
|
||||
{
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('<red>DELETING</red>');
|
||||
$anything = false;
|
||||
foreach ($this->paths_to_remove as $path) {
|
||||
$path = ROOT_DIR . $path;
|
||||
if (is_dir($path) && @Folder::delete($path)) {
|
||||
$anything = true;
|
||||
$this->output->writeln('<red>dir: </red>' . $path);
|
||||
} elseif (is_file($path) && @unlink($path)) {
|
||||
$anything = true;
|
||||
$this->output->writeln('<red>file: </red>' . $path);
|
||||
}
|
||||
}
|
||||
if (!$anything) {
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('<green>Nothing to clean...</green>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set colors style definition for the formatter.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
public function setupConsole(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
|
||||
$this->output->getFormatter()->setStyle('normal', new OutputFormatterStyle('white'));
|
||||
$this->output->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, array('bold')));
|
||||
$this->output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
|
||||
$this->output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, array('bold')));
|
||||
$this->output->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, array('bold')));
|
||||
$this->output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, array('bold')));
|
||||
$this->output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Common\Cache;
|
||||
use Grav\Console\ConsoleCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Class ClearCacheCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class ClearCacheCommand extends ConsoleCommand
|
||||
{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('clear-cache')
|
||||
->setAliases(['clearcache'])
|
||||
->setDescription('Clears Grav cache')
|
||||
->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches')
|
||||
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
|
||||
->addOption('images-only', null, InputOption::VALUE_NONE, 'If set will remove only images/*')
|
||||
->addOption('cache-only', null, InputOption::VALUE_NONE, 'If set will remove only cache/*')
|
||||
->setHelp('The <info>clear-cache</info> deletes all cache files');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function serve()
|
||||
{
|
||||
$this->cleanPaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* loops over the array of paths and deletes the files/folders
|
||||
*/
|
||||
private function cleanPaths()
|
||||
{
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('<magenta>Clearing cache</magenta>');
|
||||
$this->output->writeln('');
|
||||
|
||||
if ($this->input->getOption('all')) {
|
||||
$remove = 'all';
|
||||
} elseif ($this->input->getOption('assets-only')) {
|
||||
$remove = 'assets-only';
|
||||
} elseif ($this->input->getOption('images-only')) {
|
||||
$remove = 'images-only';
|
||||
} elseif ($this->input->getOption('cache-only')) {
|
||||
$remove = 'cache-only';
|
||||
} else {
|
||||
$remove = 'standard';
|
||||
}
|
||||
|
||||
foreach (Cache::clearCache($remove) as $result) {
|
||||
$this->output->writeln($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Console\ConsoleCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Class ComposerCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class ComposerCommand extends ConsoleCommand
|
||||
{
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $config;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $local_config;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $destination;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $user_path;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName("composer")
|
||||
->addOption(
|
||||
'install',
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
'install the dependencies'
|
||||
)
|
||||
->addOption(
|
||||
'update',
|
||||
'u',
|
||||
InputOption::VALUE_NONE,
|
||||
'update the dependencies'
|
||||
)
|
||||
->setDescription("Updates the composer vendor dependencies needed by Grav.")
|
||||
->setHelp('The <info>composer</info> command updates the composer vendor dependencies needed by Grav');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function serve()
|
||||
{
|
||||
$action = $this->input->getOption('install') ? 'install' : ($this->input->getOption('update') ? 'update' : 'install');
|
||||
|
||||
if ($this->input->getOption('install')) {
|
||||
$action = 'install';
|
||||
}
|
||||
|
||||
// Updates composer first
|
||||
$this->output->writeln("\nInstalling vendor dependencies");
|
||||
$this->output->writeln($this->composerUpdate(GRAV_ROOT, $action));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Console\ConsoleCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Class InstallCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class InstallCommand extends ConsoleCommand
|
||||
{
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $config;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $local_config;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $destination;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $user_path;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName("install")
|
||||
->addOption(
|
||||
'symlink',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
'Symlink the required bits'
|
||||
)
|
||||
->addArgument(
|
||||
'destination',
|
||||
InputArgument::OPTIONAL,
|
||||
'Where to install the required bits (default to current project)'
|
||||
)
|
||||
->setDescription("Installs the dependencies needed by Grav. Optionally can create symbolic links")
|
||||
->setHelp('The <info>install</info> command installs the dependencies needed by Grav. Optionally can create symbolic links');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function serve()
|
||||
{
|
||||
$dependencies_file = '.dependencies';
|
||||
$this->destination = ($this->input->getArgument('destination')) ? $this->input->getArgument('destination') : ROOT_DIR;
|
||||
|
||||
// fix trailing slash
|
||||
$this->destination = rtrim($this->destination, DS) . DS;
|
||||
$this->user_path = $this->destination . USER_PATH;
|
||||
|
||||
if (false === $this->isWindows()) {
|
||||
$local_config_file = exec('eval echo ~/.grav/config');
|
||||
if (file_exists($local_config_file)) {
|
||||
$this->local_config = Yaml::parse($local_config_file);
|
||||
$this->output->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
|
||||
}
|
||||
}
|
||||
|
||||
// Look for dependencies file in ROOT and USER dir
|
||||
if (file_exists($this->user_path . $dependencies_file)) {
|
||||
$this->config = Yaml::parse($this->user_path . $dependencies_file);
|
||||
} elseif (file_exists($this->destination . $dependencies_file)) {
|
||||
$this->config = Yaml::parse($this->destination . $dependencies_file);
|
||||
} else {
|
||||
$this->output->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
|
||||
}
|
||||
|
||||
// If yaml config, process
|
||||
if ($this->config) {
|
||||
if (!$this->input->getOption('symlink')) {
|
||||
// Updates composer first
|
||||
$this->output->writeln("\nInstalling vendor dependencies");
|
||||
$this->output->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
|
||||
|
||||
$this->gitclone();
|
||||
} else {
|
||||
$this->symlink();
|
||||
}
|
||||
} else {
|
||||
$this->output->writeln('<red>ERROR</red> invalid YAML in ' . $dependencies_file);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones from Git
|
||||
*/
|
||||
private function gitclone()
|
||||
{
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('<green>Cloning Bits</green>');
|
||||
$this->output->writeln('============');
|
||||
$this->output->writeln('');
|
||||
|
||||
foreach ($this->config['git'] as $repo => $data) {
|
||||
$path = $this->destination . DS . $data['path'];
|
||||
if (!file_exists($path)) {
|
||||
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' ' . $data['url'] . ' ' . $data['path']);
|
||||
$this->output->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
|
||||
$this->output->writeln('');
|
||||
} else {
|
||||
$this->output->writeln('<red>' . $path . ' already exists, skipping...</red>');
|
||||
$this->output->writeln('');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Symlinks
|
||||
*/
|
||||
private function symlink()
|
||||
{
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln('<green>Symlinking Bits</green>');
|
||||
$this->output->writeln('===============');
|
||||
$this->output->writeln('');
|
||||
|
||||
if (!$this->local_config) {
|
||||
$this->output->writeln('<red>No local configuration available, aborting...</red>');
|
||||
$this->output->writeln('');
|
||||
return;
|
||||
}
|
||||
|
||||
exec('cd ' . $this->destination);
|
||||
foreach ($this->config['links'] as $repo => $data) {
|
||||
$from = $this->local_config[$data['scm'] . '_repos'] . $data['src'];
|
||||
$to = $this->destination . $data['path'];
|
||||
|
||||
if (file_exists($from)) {
|
||||
if (!file_exists($to)) {
|
||||
symlink($from, $to);
|
||||
$this->output->writeln('<green>SUCCESS</green> symlinked <magenta>' . $data['src'] . '</magenta> -> <cyan>' . $data['path'] . '</cyan>');
|
||||
$this->output->writeln('');
|
||||
} else {
|
||||
$this->output->writeln('<red>destination: ' . $to . ' already exists, skipping...</red>');
|
||||
$this->output->writeln('');
|
||||
}
|
||||
} else {
|
||||
$this->output->writeln('<red>source: ' . $from . ' does not exists, skipping...</red>');
|
||||
$this->output->writeln('');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user