updated core to 1.7.15
This commit is contained in:
392
system/src/Grav/Installer/Install.php
Normal file
392
system/src/Grav/Installer/Install.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Installer
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Exception;
|
||||
use Grav\Common\Cache;
|
||||
use Grav\Common\GPM\Installer;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Plugins;
|
||||
use RuntimeException;
|
||||
use function class_exists;
|
||||
use function dirname;
|
||||
use function function_exists;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Grav installer.
|
||||
*
|
||||
* NOTE: This class can be initialized during upgrade from an older version of Grav. Make sure it runs there!
|
||||
*/
|
||||
final class Install
|
||||
{
|
||||
/** @var int Installer version. */
|
||||
public $version = 1;
|
||||
|
||||
/** @var array */
|
||||
public $requires = [
|
||||
'php' => [
|
||||
'name' => 'PHP',
|
||||
'versions' => [
|
||||
'7.4' => '7.4.0',
|
||||
'7.3' => '7.3.6',
|
||||
'' => '7.4.12'
|
||||
]
|
||||
],
|
||||
'grav' => [
|
||||
'name' => 'Grav',
|
||||
'versions' => [
|
||||
'1.6' => '1.6.0',
|
||||
'' => '1.6.28'
|
||||
]
|
||||
],
|
||||
'plugins' => [
|
||||
'admin' => [
|
||||
'name' => 'Admin',
|
||||
'optional' => true,
|
||||
'versions' => [
|
||||
'1.9' => '1.9.0',
|
||||
'' => '1.9.13'
|
||||
]
|
||||
],
|
||||
'email' => [
|
||||
'name' => 'Email',
|
||||
'optional' => true,
|
||||
'versions' => [
|
||||
'3.0' => '3.0.0',
|
||||
'' => '3.0.10'
|
||||
]
|
||||
],
|
||||
'form' => [
|
||||
'name' => 'Form',
|
||||
'optional' => true,
|
||||
'versions' => [
|
||||
'4.1' => '4.1.0',
|
||||
'4.0' => '4.0.0',
|
||||
'3.0' => '3.0.0',
|
||||
'' => '4.1.2'
|
||||
]
|
||||
],
|
||||
'login' => [
|
||||
'name' => 'Login',
|
||||
'optional' => true,
|
||||
'versions' => [
|
||||
'3.3' => '3.3.0',
|
||||
'3.0' => '3.0.0',
|
||||
'' => '3.3.6'
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
public $ignores = [
|
||||
'backup',
|
||||
'cache',
|
||||
'images',
|
||||
'logs',
|
||||
'tmp',
|
||||
'user',
|
||||
'.htaccess',
|
||||
'robots.txt'
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
private $classMap = [
|
||||
InstallException::class => __DIR__ . '/InstallException.php',
|
||||
Versions::class => __DIR__ . '/Versions.php',
|
||||
VersionUpdate::class => __DIR__ . '/VersionUpdate.php',
|
||||
VersionUpdater::class => __DIR__ . '/VersionUpdater.php',
|
||||
YamlUpdater::class => __DIR__ . '/YamlUpdater.php',
|
||||
];
|
||||
|
||||
/** @var string|null */
|
||||
private $zip;
|
||||
|
||||
/** @var string|null */
|
||||
private $location;
|
||||
|
||||
/** @var VersionUpdater */
|
||||
private $updater;
|
||||
|
||||
/** @var static */
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new static();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $zip
|
||||
* @return $this
|
||||
*/
|
||||
public function setZip(?string $zip)
|
||||
{
|
||||
$this->zip = $zip;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $zip
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(?string $zip)
|
||||
{
|
||||
$this->zip = $zip;
|
||||
|
||||
$failedRequirements = $this->checkRequirements();
|
||||
if ($failedRequirements) {
|
||||
$error = ['Following requirements have failed:'];
|
||||
|
||||
foreach ($failedRequirements as $name => $req) {
|
||||
$error[] = "{$req['title']} >= <strong>v{$req['minimum']}</strong> required, you have <strong>v{$req['installed']}</strong>";
|
||||
}
|
||||
|
||||
$errors = implode("<br />\n", $error);
|
||||
if (\defined('GRAV_CLI') && GRAV_CLI) {
|
||||
$errors = "\n\n" . strip_tags($errors) . "\n\n";
|
||||
$errors .= <<<ERR
|
||||
Please install Grav 1.6.31 first by running following commands:
|
||||
|
||||
wget -q https://getgrav.org/download/core/grav-update/1.6.31 -O tmp/grav-update-v1.6.31.zip
|
||||
bin/gpm direct-install -y tmp/grav-update-v1.6.31.zip
|
||||
rm tmp/grav-update.zip
|
||||
ERR;
|
||||
}
|
||||
|
||||
throw new RuntimeException($errors);
|
||||
}
|
||||
|
||||
$this->prepare();
|
||||
$this->install();
|
||||
$this->finalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This method can only be called after $grav['plugins']->init().
|
||||
*
|
||||
* @return array List of failed requirements. If the list is empty, installation can go on.
|
||||
*/
|
||||
public function checkRequirements(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$this->checkVersion($results, 'php', 'php', $this->requires['php'], PHP_VERSION);
|
||||
$this->checkVersion($results, 'grav', 'grav', $this->requires['grav'], GRAV_VERSION);
|
||||
$this->checkPlugins($results, $this->requires['plugins']);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function prepare(): void
|
||||
{
|
||||
// Locate the new Grav update and the target site from the filesystem.
|
||||
$location = realpath(__DIR__);
|
||||
$target = realpath(GRAV_ROOT . '/index.php');
|
||||
|
||||
if (!$location) {
|
||||
throw new RuntimeException('Internal Error', 500);
|
||||
}
|
||||
|
||||
if ($target && dirname($location, 4) === dirname($target)) {
|
||||
// We cannot copy files into themselves, abort!
|
||||
throw new RuntimeException('Grav has already been installed here!', 400);
|
||||
}
|
||||
|
||||
// Load the installer classes.
|
||||
foreach ($this->classMap as $class_name => $path) {
|
||||
// Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail!
|
||||
if (class_exists($class_name, false)) {
|
||||
throw new RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500);
|
||||
}
|
||||
|
||||
require $path;
|
||||
}
|
||||
|
||||
$this->legacySupport();
|
||||
|
||||
$this->location = dirname($location, 4);
|
||||
|
||||
$versions = Versions::instance(USER_DIR . 'config/versions.yaml');
|
||||
$this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', $this->getVersion(), $versions);
|
||||
|
||||
$this->updater->preflight();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function install(): void
|
||||
{
|
||||
if (!$this->location) {
|
||||
throw new RuntimeException('Oops, installer was run without prepare()!', 500);
|
||||
}
|
||||
|
||||
try {
|
||||
// Update user/config/version.yaml before copying the files to avoid frontend from setting the version schema.
|
||||
$this->updater->install();
|
||||
|
||||
Installer::install(
|
||||
$this->zip ?? '',
|
||||
GRAV_ROOT,
|
||||
['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $this->ignores],
|
||||
$this->location,
|
||||
!($this->zip && is_file($this->zip))
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
Installer::setError($e->getMessage());
|
||||
}
|
||||
|
||||
$errorCode = Installer::lastErrorCode();
|
||||
|
||||
$success = !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)));
|
||||
|
||||
if (!$success) {
|
||||
throw new RuntimeException(Installer::lastErrorMsg());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function finalize(): void
|
||||
{
|
||||
// Finalize can be run without installing Grav first.
|
||||
if (!$this->updater) {
|
||||
$versions = Versions::instance(USER_DIR . 'config/versions.yaml');
|
||||
$this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', GRAV_VERSION, $versions);
|
||||
$this->updater->install();
|
||||
}
|
||||
|
||||
$this->updater->postflight();
|
||||
|
||||
Cache::clearCache('all');
|
||||
|
||||
clearstatcache();
|
||||
if (function_exists('opcache_reset')) {
|
||||
@opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $results
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param array $check
|
||||
* @param string|null $version
|
||||
* @return void
|
||||
*/
|
||||
protected function checkVersion(array &$results, $type, $name, array $check, $version): void
|
||||
{
|
||||
if (null === $version && !empty($check['optional'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$major = $minor = 0;
|
||||
$versions = $check['versions'] ?? [];
|
||||
foreach ($versions as $major => $minor) {
|
||||
if (!$major || version_compare($version ?? '0', $major, '<')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (version_compare($version ?? '0', $minor, '>=')) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$major) {
|
||||
$minor = reset($versions);
|
||||
}
|
||||
|
||||
$recommended = end($versions);
|
||||
|
||||
if (version_compare($recommended, $minor, '<=')) {
|
||||
$recommended = null;
|
||||
}
|
||||
|
||||
$results[$name] = [
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'title' => $check['name'] ?? $name,
|
||||
'installed' => $version,
|
||||
'minimum' => $minor,
|
||||
'recommended' => $recommended
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $results
|
||||
* @param array $plugins
|
||||
* @return void
|
||||
*/
|
||||
protected function checkPlugins(array &$results, array $plugins): void
|
||||
{
|
||||
if (!class_exists('Plugins')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($plugins as $name => $check) {
|
||||
$plugin = Plugins::get($name);
|
||||
if (!$plugin) {
|
||||
$this->checkVersion($results, 'plugin', $name, $check, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
$blueprint = $plugin->blueprints();
|
||||
$version = (string)$blueprint->get('version');
|
||||
$check['name'] = ($blueprint->get('name') ?? $check['name'] ?? $name) . ' Plugin';
|
||||
$this->checkVersion($results, 'plugin', $name, $check, $version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getVersion(): string
|
||||
{
|
||||
$definesFile = "{$this->location}/system/defines.php";
|
||||
$content = file_get_contents($definesFile);
|
||||
if (false === $content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
preg_match("/define\('GRAV_VERSION', '([^']+)'\);/mu", $content, $matches);
|
||||
|
||||
return $matches[1] ?? '';
|
||||
}
|
||||
|
||||
protected function legacySupport(): void
|
||||
{
|
||||
// Support install for Grav 1.6.0 - 1.6.20 by loading the original class from the older version of Grav.
|
||||
class_exists(\Grav\Console\Cli\CacheCommand::class, true);
|
||||
}
|
||||
}
|
29
system/src/Grav/Installer/InstallException.php
Normal file
29
system/src/Grav/Installer/InstallException.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Installer
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class InstallException
|
||||
* @package Grav\Installer
|
||||
*/
|
||||
class InstallException extends \RuntimeException
|
||||
{
|
||||
/**
|
||||
* InstallException constructor.
|
||||
* @param string $message
|
||||
* @param Throwable $previous
|
||||
*/
|
||||
public function __construct(string $message, Throwable $previous)
|
||||
{
|
||||
parent::__construct($message, $previous->getCode(), $previous);
|
||||
}
|
||||
}
|
82
system/src/Grav/Installer/VersionUpdate.php
Normal file
82
system/src/Grav/Installer/VersionUpdate.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Class VersionUpdate
|
||||
* @package Grav\Installer
|
||||
*/
|
||||
final class VersionUpdate
|
||||
{
|
||||
/** @var string */
|
||||
private $revision;
|
||||
/** @var string */
|
||||
private $version;
|
||||
/** @var string */
|
||||
private $date;
|
||||
/** @var string */
|
||||
private $patch;
|
||||
/** @var VersionUpdater */
|
||||
private $updater;
|
||||
/** @var callable[] */
|
||||
private $methods;
|
||||
|
||||
public function __construct(string $file, VersionUpdater $updater)
|
||||
{
|
||||
$name = basename($file, '.php');
|
||||
|
||||
$this->revision = $name;
|
||||
[$this->version, $this->date, $this->patch] = explode('_', $name);
|
||||
$this->updater = $updater;
|
||||
$this->methods = require $file;
|
||||
}
|
||||
|
||||
public function getRevision(): string
|
||||
{
|
||||
return $this->revision;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function getDate(): string
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function getPatch(): string
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function getUpdater(): VersionUpdater
|
||||
{
|
||||
return $this->updater;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run right before installation.
|
||||
*/
|
||||
public function preflight(VersionUpdater $updater): void
|
||||
{
|
||||
$method = $this->methods['preflight'] ?? null;
|
||||
if ($method instanceof Closure) {
|
||||
$method->call($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs right after installation.
|
||||
*/
|
||||
public function postflight(VersionUpdater $updater): void
|
||||
{
|
||||
$method = $this->methods['postflight'] ?? null;
|
||||
if ($method instanceof Closure) {
|
||||
$method->call($this);
|
||||
}
|
||||
}
|
||||
}
|
133
system/src/Grav/Installer/VersionUpdater.php
Normal file
133
system/src/Grav/Installer/VersionUpdater.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use DirectoryIterator;
|
||||
|
||||
/**
|
||||
* Class VersionUpdater
|
||||
* @package Grav\Installer
|
||||
*/
|
||||
final class VersionUpdater
|
||||
{
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var string */
|
||||
private $path;
|
||||
/** @var string */
|
||||
private $version;
|
||||
/** @var Versions */
|
||||
private $versions;
|
||||
/** @var VersionUpdate[] */
|
||||
private $updates;
|
||||
|
||||
/**
|
||||
* VersionUpdater constructor.
|
||||
* @param string $name
|
||||
* @param string $path
|
||||
* @param string $version
|
||||
* @param Versions $versions
|
||||
*/
|
||||
public function __construct(string $name, string $path, string $version, Versions $versions)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->path = $path;
|
||||
$this->version = $version;
|
||||
$this->versions = $versions;
|
||||
|
||||
$this->loadUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-installation method.
|
||||
*/
|
||||
public function preflight(): void
|
||||
{
|
||||
foreach ($this->updates as $revision => $update) {
|
||||
$update->preflight($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install method.
|
||||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$versions = $this->getVersions();
|
||||
$versions->updateVersion($this->name, $this->version);
|
||||
$versions->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-installation method.
|
||||
*/
|
||||
public function postflight(): void
|
||||
{
|
||||
$versions = $this->getVersions();
|
||||
|
||||
foreach ($this->updates as $revision => $update) {
|
||||
$update->postflight($this);
|
||||
|
||||
$versions->setSchema($this->name, $revision);
|
||||
$versions->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Versions
|
||||
*/
|
||||
public function getVersions(): Versions
|
||||
{
|
||||
return $this->versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return string|null
|
||||
*/
|
||||
public function getExtensionVersion(string $name = null): ?string
|
||||
{
|
||||
return $this->versions->getVersion($name ?? $this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return string|null
|
||||
*/
|
||||
public function getExtensionSchema(string $name = null): ?string
|
||||
{
|
||||
return $this->versions->getSchema($name ?? $this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return array
|
||||
*/
|
||||
public function getExtensionHistory(string $name = null): array
|
||||
{
|
||||
return $this->versions->getHistory($name ?? $this->name);
|
||||
}
|
||||
|
||||
protected function loadUpdates(): void
|
||||
{
|
||||
$this->updates = [];
|
||||
|
||||
$schema = $this->getExtensionSchema();
|
||||
$iterator = new DirectoryIterator($this->path);
|
||||
foreach ($iterator as $item) {
|
||||
if (!$item->isFile() || $item->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$revision = $item->getBasename('.php');
|
||||
if (!$schema || version_compare($revision, $schema, '>')) {
|
||||
$realPath = $item->getRealPath();
|
||||
if ($realPath) {
|
||||
$this->updates[$revision] = new VersionUpdate($realPath, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uksort($this->updates, 'version_compare');
|
||||
}
|
||||
}
|
329
system/src/Grav/Installer/Versions.php
Normal file
329
system/src/Grav/Installer/Versions.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Installer
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Grav Versions
|
||||
*
|
||||
* NOTE: This class can be initialized during upgrade from an older version of Grav. Make sure it runs there!
|
||||
*/
|
||||
final class Versions
|
||||
{
|
||||
/** @var string */
|
||||
protected $filename;
|
||||
/** @var array */
|
||||
protected $items;
|
||||
/** @var bool */
|
||||
protected $updated = false;
|
||||
|
||||
/** @var self[] */
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* @param string|null $filename
|
||||
* @return self
|
||||
*/
|
||||
public static function instance(string $filename = null): self
|
||||
{
|
||||
$filename = $filename ?? USER_DIR . 'config/versions.yaml';
|
||||
|
||||
if (!isset(self::$instance[$filename])) {
|
||||
self::$instance[$filename] = new self($filename);
|
||||
}
|
||||
|
||||
return self::$instance[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if the file was updated.
|
||||
*/
|
||||
public function save(): bool
|
||||
{
|
||||
if (!$this->updated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file_put_contents($this->filename, Yaml::dump($this->items, 4, 2));
|
||||
|
||||
$this->updated = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getGrav(): ?array
|
||||
{
|
||||
return $this->get('core/grav');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getPlugins(): array
|
||||
{
|
||||
return $this->get('plugins', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function getPlugin(string $name): ?array
|
||||
{
|
||||
return $this->get("plugins/{$name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getThemes(): array
|
||||
{
|
||||
return $this->get('themes', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTheme(string $name): ?array
|
||||
{
|
||||
return $this->get("themes/{$name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @return array|null
|
||||
*/
|
||||
public function getExtension(string $extension): ?array
|
||||
{
|
||||
return $this->get($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @param array|null $value
|
||||
*/
|
||||
public function setExtension(string $extension, ?array $value): void
|
||||
{
|
||||
if (null !== $value) {
|
||||
$this->set($extension, $value);
|
||||
} else {
|
||||
$this->undef($extension);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @return string|null
|
||||
*/
|
||||
public function getVersion(string $extension): ?string
|
||||
{
|
||||
$version = $this->get("{$extension}/version", null);
|
||||
|
||||
return is_string($version) ? $version : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @param string|null $version
|
||||
*/
|
||||
public function setVersion(string $extension, ?string $version): void
|
||||
{
|
||||
$this->updateHistory($extension, $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Updates also history.
|
||||
*
|
||||
* @param string $extension
|
||||
* @param string|null $version
|
||||
*/
|
||||
public function updateVersion(string $extension, ?string $version): void
|
||||
{
|
||||
$this->set("{$extension}/version", $version);
|
||||
$this->updateHistory($extension, $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSchema(string $extension): ?string
|
||||
{
|
||||
$version = $this->get("{$extension}/schema", null);
|
||||
|
||||
return is_string($version) ? $version : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @param string|null $schema
|
||||
*/
|
||||
public function setSchema(string $extension, ?string $schema): void
|
||||
{
|
||||
if (null !== $schema) {
|
||||
$this->set("{$extension}/schema", $schema);
|
||||
} else {
|
||||
$this->undef("{$extension}/schema");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @return array
|
||||
*/
|
||||
public function getHistory(string $extension): array
|
||||
{
|
||||
$name = "{$extension}/history";
|
||||
$history = $this->get($name, []);
|
||||
|
||||
// Fix for broken Grav 1.6 history
|
||||
if ($extension === 'grav') {
|
||||
$history = $this->fixHistory($history);
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extension
|
||||
* @param string|null $version
|
||||
*/
|
||||
public function updateHistory(string $extension, ?string $version): void
|
||||
{
|
||||
$name = "{$extension}/history";
|
||||
$history = $this->getHistory($extension);
|
||||
$history[] = ['version' => $version, 'date' => gmdate('Y-m-d H:i:s')];
|
||||
$this->set($name, $history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears extension history. Useful when creating skeletons.
|
||||
*
|
||||
* @param string $extension
|
||||
*/
|
||||
public function removeHistory(string $extension): void
|
||||
{
|
||||
$this->undef("{$extension}/history");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $history
|
||||
* @return array
|
||||
*/
|
||||
private function fixHistory(array $history): array
|
||||
{
|
||||
if (isset($history['version'], $history['date'])) {
|
||||
$fix = [['version' => $history['version'], 'date' => $history['date']]];
|
||||
unset($history['version'], $history['date']);
|
||||
$history = array_merge($fix, $history);
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Slash separated path to the requested value.
|
||||
* @param mixed $default Default value (or null).
|
||||
* @return mixed Value.
|
||||
*/
|
||||
private function get(string $name, $default = null)
|
||||
{
|
||||
$path = explode('/', $name);
|
||||
$current = $this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (is_array($current) && isset($current[$field])) {
|
||||
$current = $current[$field];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Slash separated path to the requested value.
|
||||
* @param mixed $value New value.
|
||||
*/
|
||||
private function set(string $name, $value): void
|
||||
{
|
||||
$path = explode('/', $name);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
// Handle arrays and scalars.
|
||||
if (!is_array($current)) {
|
||||
$current = [$field => []];
|
||||
} elseif (!isset($current[$field])) {
|
||||
$current[$field] = [];
|
||||
}
|
||||
$current = &$current[$field];
|
||||
}
|
||||
|
||||
$current = $value;
|
||||
$this->updated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
*/
|
||||
private function undef(string $name): void
|
||||
{
|
||||
$path = $name !== '' ? explode('/', $name) : [];
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
|
||||
$var = array_pop($path);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (!is_array($current) || !isset($current[$field])) {
|
||||
return;
|
||||
}
|
||||
$current = &$current[$field];
|
||||
}
|
||||
|
||||
unset($current[$var]);
|
||||
$this->updated = true;
|
||||
}
|
||||
|
||||
private function __construct(string $filename)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$content = is_file($filename) ? file_get_contents($filename) : null;
|
||||
if (false === $content) {
|
||||
throw new \RuntimeException('Versions file cannot be read');
|
||||
}
|
||||
$this->items = $content ? Yaml::parse($content) : [];
|
||||
}
|
||||
}
|
430
system/src/Grav/Installer/YamlUpdater.php
Normal file
430
system/src/Grav/Installer/YamlUpdater.php
Normal file
@@ -0,0 +1,430 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Installer
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Installer;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Grav YAML updater.
|
||||
*
|
||||
* NOTE: This class can be initialized during upgrade from an older version of Grav. Make sure it runs there!
|
||||
*/
|
||||
final class YamlUpdater
|
||||
{
|
||||
/** @var string */
|
||||
protected $filename;
|
||||
/** @var string[] */
|
||||
protected $lines;
|
||||
/** @var array */
|
||||
protected $comments;
|
||||
/** @var array */
|
||||
protected $items;
|
||||
/** @var bool */
|
||||
protected $updated = false;
|
||||
|
||||
/** @var self[] */
|
||||
protected static $instance;
|
||||
|
||||
public static function instance(string $filename): self
|
||||
{
|
||||
if (!isset(self::$instance[$filename])) {
|
||||
self::$instance[$filename] = new self($filename);
|
||||
}
|
||||
|
||||
return self::$instance[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function save(): bool
|
||||
{
|
||||
if (!$this->updated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->isHandWritten()) {
|
||||
$yaml = Yaml::dump($this->items, 5, 2);
|
||||
} else {
|
||||
$yaml = implode("\n", $this->lines);
|
||||
|
||||
$items = Yaml::parse($yaml);
|
||||
if ($items !== $this->items) {
|
||||
throw new \RuntimeException('Failed saving the content');
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($this->filename, $yaml);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException('Failed to update ' . basename($this->filename) . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isHandWritten(): bool
|
||||
{
|
||||
return !empty($this->comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getComments(): array
|
||||
{
|
||||
$comments = [];
|
||||
foreach ($this->lines as $i => $line) {
|
||||
if ($this->isLineEmpty($line)) {
|
||||
$comments[$i+1] = $line;
|
||||
} elseif ($comment = $this->getInlineComment($line)) {
|
||||
$comments[$i+1] = $comment;
|
||||
}
|
||||
}
|
||||
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $variable
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function define(string $variable, $value): void
|
||||
{
|
||||
// If variable has already value, we're good.
|
||||
if ($this->get($variable) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If one of the parents isn't array, we're good, too.
|
||||
if (!$this->canDefine($variable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set($variable, $value);
|
||||
if (!$this->isHandWritten()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = explode('.', $variable);
|
||||
|
||||
$lineNos = $this->findPath($this->lines, $parts);
|
||||
$count = count($lineNos);
|
||||
$last = array_key_last($lineNos);
|
||||
|
||||
$value = explode("\n", trim(Yaml::dump([$last => $this->get(implode('.', array_keys($lineNos)))], max(0, 5-$count), 2)));
|
||||
$currentLine = array_pop($lineNos) ?: 0;
|
||||
$parentLine = array_pop($lineNos);
|
||||
|
||||
if ($parentLine !== null) {
|
||||
$c = $this->getLineIndentation($this->lines[$parentLine] ?? '');
|
||||
$n = $this->getLineIndentation($this->lines[$parentLine+1] ?? $this->lines[$parentLine] ?? '');
|
||||
$indent = $n > $c ? $n : $c + 2;
|
||||
} else {
|
||||
$indent = 0;
|
||||
array_unshift($value, '');
|
||||
}
|
||||
$spaces = str_repeat(' ', $indent);
|
||||
foreach ($value as &$line) {
|
||||
$line = $spaces . $line;
|
||||
}
|
||||
unset($line);
|
||||
|
||||
array_splice($this->lines, abs($currentLine)+1, 0, $value);
|
||||
}
|
||||
|
||||
public function undefine(string $variable): void
|
||||
{
|
||||
// If variable does not have value, we're good.
|
||||
if ($this->get($variable) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If one of the parents isn't array, we're good, too.
|
||||
if (!$this->canDefine($variable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->undef($variable);
|
||||
if (!$this->isHandWritten()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support also removing property from handwritten configuration file.
|
||||
}
|
||||
|
||||
private function __construct(string $filename)
|
||||
{
|
||||
$content = is_file($filename) ? (string)file_get_contents($filename) : '';
|
||||
$content = rtrim(str_replace(["\r\n", "\r"], "\n", $content));
|
||||
|
||||
$this->filename = $filename;
|
||||
$this->lines = explode("\n", $content);
|
||||
$this->comments = $this->getComments();
|
||||
$this->items = $content ? Yaml::parse($content) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of offsets for the parent nodes. Negative value means position, but not found.
|
||||
*
|
||||
* @param array $lines
|
||||
* @param array $parts
|
||||
* @return int[]
|
||||
*/
|
||||
private function findPath(array $lines, array $parts)
|
||||
{
|
||||
$test = true;
|
||||
$indent = -1;
|
||||
$current = array_shift($parts);
|
||||
|
||||
$j = 1;
|
||||
$found = [];
|
||||
$space = '';
|
||||
foreach ($lines as $i => $line) {
|
||||
if ($this->isLineEmpty($line)) {
|
||||
if ($this->isLineComment($line) && $this->getLineIndentation($line) > $indent) {
|
||||
$j = $i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test === true) {
|
||||
$test = false;
|
||||
$spaces = strlen($line) - strlen(ltrim($line, ' '));
|
||||
if ($spaces <= $indent) {
|
||||
$found[$current] = -$j;
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
$indent = $spaces;
|
||||
$space = $indent ? str_repeat(' ', $indent) : '';
|
||||
}
|
||||
|
||||
|
||||
if (0 === \strncmp($line, $space, strlen($space))) {
|
||||
$pattern = "/^{$space}(['\"]?){$current}\\1\:/";
|
||||
|
||||
if (preg_match($pattern, $line)) {
|
||||
$found[$current] = $i;
|
||||
$current = array_shift($parts);
|
||||
if ($current === null) {
|
||||
return $found;
|
||||
}
|
||||
$test = true;
|
||||
}
|
||||
} else {
|
||||
$found[$current] = -$j;
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
$j = $i;
|
||||
}
|
||||
|
||||
$found[$current] = -$j;
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current line is blank or if it is a comment line.
|
||||
*
|
||||
* @param string $line Contents of the line
|
||||
* @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
|
||||
*/
|
||||
private function isLineEmpty(string $line): bool
|
||||
{
|
||||
return $this->isLineBlank($line) || $this->isLineComment($line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current line is blank.
|
||||
*
|
||||
* @param string $line Contents of the line
|
||||
* @return bool Returns true if the current line is blank, false otherwise
|
||||
*/
|
||||
private function isLineBlank(string $line): bool
|
||||
{
|
||||
return '' === trim($line, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current line is a comment line.
|
||||
*
|
||||
* @param string $line Contents of the line
|
||||
* @return bool Returns true if the current line is a comment line, false otherwise
|
||||
*/
|
||||
private function isLineComment(string $line): bool
|
||||
{
|
||||
//checking explicitly the first char of the trim is faster than loops or strpos
|
||||
$ltrimmedLine = ltrim($line, ' ');
|
||||
|
||||
return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $line
|
||||
* @return bool
|
||||
*/
|
||||
private function isInlineComment(string $line): bool
|
||||
{
|
||||
return $this->getInlineComment($line) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $line
|
||||
* @return string|null
|
||||
*/
|
||||
private function getInlineComment(string $line): ?string
|
||||
{
|
||||
$pos = strpos($line, ' #');
|
||||
if (false === $pos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode(' #', $line);
|
||||
$part = '';
|
||||
while ($part .= array_shift($parts)) {
|
||||
// Remove quoted values.
|
||||
$part = preg_replace('/(([\'"])[^\2]*\2)/', '', $part);
|
||||
assert(null !== $part);
|
||||
$part = preg_split('/[\'"]/', $part, 2);
|
||||
assert(false !== $part);
|
||||
if (!isset($part[1])) {
|
||||
$part = $part[0];
|
||||
array_unshift($parts, str_repeat(' ', strlen($part) - strlen(trim($part, ' '))));
|
||||
break;
|
||||
}
|
||||
$part = $part[1];
|
||||
}
|
||||
|
||||
|
||||
return implode(' #', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current line indentation.
|
||||
*
|
||||
* @param string $line
|
||||
* @return int The current line indentation
|
||||
*/
|
||||
private function getLineIndentation(string $line): int
|
||||
{
|
||||
return \strlen($line) - \strlen(ltrim($line, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $default Default value (or null).
|
||||
* @return mixed Value.
|
||||
*/
|
||||
private function get(string $name, $default = null)
|
||||
{
|
||||
$path = explode('.', $name);
|
||||
$current = $this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (is_array($current) && isset($current[$field])) {
|
||||
$current = $current[$field];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @param mixed $value New value.
|
||||
*/
|
||||
private function set(string $name, $value): void
|
||||
{
|
||||
$path = explode('.', $name);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
// Handle arrays and scalars.
|
||||
if (!is_array($current)) {
|
||||
$current = [$field => []];
|
||||
} elseif (!isset($current[$field])) {
|
||||
$current[$field] = [];
|
||||
}
|
||||
$current = &$current[$field];
|
||||
}
|
||||
|
||||
$current = $value;
|
||||
$this->updated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
*/
|
||||
private function undef(string $name): void
|
||||
{
|
||||
$path = $name !== '' ? explode('.', $name) : [];
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
|
||||
$var = array_pop($path);
|
||||
$current = &$this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (!is_array($current) || !isset($current[$field])) {
|
||||
return;
|
||||
}
|
||||
$current = &$current[$field];
|
||||
}
|
||||
|
||||
unset($current[$var]);
|
||||
$this->updated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value by using dot notation for nested arrays/objects.
|
||||
*
|
||||
* @param string $name Dot separated path to the requested value.
|
||||
* @return bool
|
||||
*/
|
||||
private function canDefine(string $name): bool
|
||||
{
|
||||
$path = explode('.', $name);
|
||||
$current = $this->items;
|
||||
|
||||
foreach ($path as $field) {
|
||||
if (is_array($current)) {
|
||||
if (!isset($current[$field])) {
|
||||
return true;
|
||||
}
|
||||
$current = $current[$field];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
24
system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php
Normal file
24
system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Grav\Installer\InstallException;
|
||||
use Grav\Installer\VersionUpdate;
|
||||
use Grav\Installer\YamlUpdater;
|
||||
|
||||
return [
|
||||
'preflight' => null,
|
||||
'postflight' =>
|
||||
function () {
|
||||
/** @var VersionUpdate $this */
|
||||
try {
|
||||
// Keep old defaults for backwards compatibility.
|
||||
$yaml = YamlUpdater::instance(GRAV_ROOT . '/user/config/system.yaml');
|
||||
$yaml->define('twig.autoescape', false);
|
||||
$yaml->define('strict_mode.yaml_compat', true);
|
||||
$yaml->define('strict_mode.twig_compat', true);
|
||||
$yaml->define('strict_mode.blueprint_compat', true);
|
||||
$yaml->save();
|
||||
} catch (\Exception $e) {
|
||||
throw new InstallException('Could not update system configuration to maintain backwards compatibility', $e);
|
||||
}
|
||||
}
|
||||
];
|
Reference in New Issue
Block a user