Merge branch 'release/1.7.50.1'

This commit is contained in:
Andy Miller
2025-10-20 13:38:28 -06:00
13 changed files with 1031 additions and 60 deletions

View File

@@ -1,3 +1,9 @@
# v1.7.50.1
## 10/20/2025
1. [](#bugfix)
* Fix for broken `GRAV_ROOT`
# v1.7.50
## 10/19/2025

View File

@@ -11,9 +11,6 @@ namespace Grav;
\define('GRAV_REQUEST_TIME', microtime(true));
\define('GRAV_PHP_MIN', '7.3.6');
if (!\defined('GRAV_ROOT')) {
\define('GRAV_ROOT', __DIR__);
}
if (PHP_SAPI === 'cli-server') {
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
@@ -97,6 +94,13 @@ $grav = Grav::instance(array('loader' => $loader));
try {
$grav->process();
} catch (\Error|\Exception $e) {
$grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
$grav->fireEvent('onFatalException', new Event(['exception' => $e]));
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag)) {
require __DIR__ . '/system/recovery.php';
return 0;
}
throw $e;
}

505
needs_fixing.txt Normal file
View File

@@ -0,0 +1,505 @@
------ ----------------------------------------------------
Line src/Grav/Common/GPM/Response.php
------ ----------------------------------------------------
3 Class Grav\Common\GPM\Response not found.
🪪 class.notFound
💡 Learn more at
https://phpstan.org/user-guide/discovering-symbols
------ ----------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Common/Grav.php
------ -----------------------------------------------------------
148 No error to ignore is reported on line 148.
681 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Common/Page/Collection.php
------ -----------------------------------------------------------
112 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
209 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Common/Processors/InitializeProcessor.php
------ -----------------------------------------------------------
58 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -------------------------------------------------------
Line src/Grav/Common/Scheduler/Job.php
------ -------------------------------------------------------
574 Call to static method sendEmail() on an unknown class
Grav\Plugin\Email\Utils.
🪪 class.notFound
💡 Learn more at
https://phpstan.org/user-guide/discovering-symbols
------ -------------------------------------------------------
------ ----------------------------------------------------------
Line src/Grav/Common/Scheduler/SchedulerController.php
------ ----------------------------------------------------------
41 Class Grav\Common\Scheduler\ModernScheduler not found.
🪪 class.notFound
💡 Learn more at
https://phpstan.org/user-guide/discovering-symbols
45 Instantiated class Grav\Common\Scheduler\ModernScheduler
not found.
🪪 class.notFound
💡 Learn more at
https://phpstan.org/user-guide/discovering-symbols
------ ----------------------------------------------------------
------ --------------------------------------------------------
Line src/Grav/Common/Service/SchedulerServiceProvider.php
------ --------------------------------------------------------
55 Instantiated class Grav\Common\Scheduler\JobWorker not
found.
🪪 class.notFound
💡 Learn more at
https://phpstan.org/user-guide/discovering-symbols
------ --------------------------------------------------------
------ ---------------------------------------------
Line src/Grav/Common/Session.php
------ ---------------------------------------------
132 No error to ignore is reported on line 132.
137 No error to ignore is reported on line 137.
------ ---------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Common/Uri.php
------ -----------------------------------------------------------
1131 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ --------------------------------------------------------
Line src/Grav/Console/Application/Application.php
------ --------------------------------------------------------
125 Return type mixed of method
Grav\Console\Application\Application::configureIO() is
not covariant with return type void of method
Symfony\Component\Console\Application::configureIO().
------ --------------------------------------------------------
------ ---------------------------------------------------------
Line src/Grav/Console/ConsoleCommand.php
------ ---------------------------------------------------------
29 Return type mixed of method
Grav\Console\ConsoleCommand::execute() is not covariant
with return type int of method
Symfony\Component\Console\Command\Command::execute().
------ ---------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Console/ConsoleTrait.php (in context of class
Grav\Console\ConsoleCommand)
------ -----------------------------------------------------------
89 Method Grav\Console\ConsoleCommand::addOption() overrides
method
Symfony\Component\Console\Command\Command::addOption()
but misses parameter #6 $suggestedValues.
------ -----------------------------------------------------------
------ --------------------------------------------------------
Line src/Grav/Console/ConsoleTrait.php (in context of class
Grav\Console\GpmCommand)
------ --------------------------------------------------------
89 Method Grav\Console\GpmCommand::addOption() overrides
method
Symfony\Component\Console\Command\Command::addOption()
but misses parameter #6 $suggestedValues.
------ --------------------------------------------------------
------ --------------------------------------------------------
Line src/Grav/Console/ConsoleTrait.php (in context of class
Grav\Console\GravCommand)
------ --------------------------------------------------------
89 Method Grav\Console\GravCommand::addOption() overrides
method
Symfony\Component\Console\Command\Command::addOption()
but misses parameter #6 $suggestedValues.
------ --------------------------------------------------------
------ ----------------------------------------------------------
Line src/Grav/Console/GpmCommand.php
------ ----------------------------------------------------------
31 Return type mixed of method
Grav\Console\GpmCommand::execute() is not covariant with
return type int of method
Symfony\Component\Console\Command\Command::execute().
39 No error to ignore is reported on line 39.
------ ----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Console/GravCommand.php
------ -----------------------------------------------------------
29 Return type mixed of method
Grav\Console\GravCommand::execute() is not covariant with
return type int of method
Symfony\Component\Console\Command\Command::execute().
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Acl/RecursiveActionIterator.php
------ -----------------------------------------------------------
62 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ ----------------------------------------------------------
Line src/Grav/Framework/Cache/CacheTrait.php (in context of
class Grav\Framework\Cache\AbstractCache)
------ ----------------------------------------------------------
87 Return type mixed of method
Grav\Framework\Cache\AbstractCache::get() is not
covariant with return type mixed of method
Psr\SimpleCache\CacheInterface::get().
102 Return type mixed of method
Grav\Framework\Cache\AbstractCache::set() is not
covariant with return type bool of method
Psr\SimpleCache\CacheInterface::set().
117 Return type mixed of method
Grav\Framework\Cache\AbstractCache::delete() is not
covariant with return type bool of method
Psr\SimpleCache\CacheInterface::delete().
127 Return type mixed of method
Grav\Framework\Cache\AbstractCache::clear() is not
covariant with return type bool of method
Psr\SimpleCache\CacheInterface::clear().
138 Return type mixed of method
Grav\Framework\Cache\AbstractCache::getMultiple() is not
covariant with return type iterable of method
Psr\SimpleCache\CacheInterface::getMultiple().
181 Return type mixed of method
Grav\Framework\Cache\AbstractCache::setMultiple() is not
covariant with return type bool of method
Psr\SimpleCache\CacheInterface::setMultiple().
214 Return type mixed of method
Grav\Framework\Cache\AbstractCache::deleteMultiple() is
not covariant with return type bool of method
Psr\SimpleCache\CacheInterface::deleteMultiple().
242 Return type mixed of method
Grav\Framework\Cache\AbstractCache::has() is not
covariant with return type bool of method
Psr\SimpleCache\CacheInterface::has().
------ ----------------------------------------------------------
------ ----------------------------------------------------------
Line src/Grav/Framework/Collection/AbstractFileCollection.php
------ ----------------------------------------------------------
95 No error to ignore is reported on line 95.
------ ----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Collection/AbstractIndexCollection.php
------ -----------------------------------------------------------
154 No error to ignore is reported on line 154.
168 No error to ignore is reported on line 168.
185 No error to ignore is reported on line 185.
201 No error to ignore is reported on line 201.
507 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Collection/AbstractLazyCollection.php
------ -----------------------------------------------------------
29 Property
Grav\Framework\Collection\AbstractLazyCollection::$collec
tion overriding property
Doctrine\Common\Collections\AbstractLazyCollection<TKey o
f (int|string),T>::$collection (Doctrine\Common\Collectio
ns\Collection|null) should also have native type
Doctrine\Common\Collections\Collection|null.
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Contracts/Relationships/RelationshipIn
terface.php
------ -----------------------------------------------------------
80 Return type iterable of method
Grav\Framework\Contracts\Relationships\RelationshipInterf
ace::getIterator() is not covariant with tentative return
type Traversable of method IteratorAggregate<string,T of
Grav\Framework\Contracts\Object\IdentifierInterface>::get
Iterator().
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
attribute to temporarily suppress the error.
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Filesystem/Filesystem.php
------ -----------------------------------------------------------
51 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
252 No error to ignore is reported on line 252.
------ -----------------------------------------------------------
------ ---------------------------------------------
Line src/Grav/Framework/Flex/FlexCollection.php
------ ---------------------------------------------
102 No error to ignore is reported on line 102.
------ ---------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Flex/FlexIdentifier.php
------ -----------------------------------------------------------
27 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ ----------------------------------------------------------
Line src/Grav/Framework/Flex/FlexIndex.php
------ ----------------------------------------------------------
109 No error to ignore is reported on line 109.
934 Method Grav\Framework\Flex\FlexIndex::reduce() should
return TInitial|TReturn but return statement is missing.
🪪 return.missing
------ ----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Form/FormFlashFile.php
------ -----------------------------------------------------------
62 Return type mixed of method
Grav\Framework\Form\FormFlashFile::getStream() is not
covariant with return type
Psr\Http\Message\StreamInterface of method
Psr\Http\Message\UploadedFileInterface::getStream().
83 Return type mixed of method
Grav\Framework\Form\FormFlashFile::moveTo() is not
covariant with return type void of method
Psr\Http\Message\UploadedFileInterface::moveTo().
123 Return type mixed of method
Grav\Framework\Form\FormFlashFile::getSize() is not
covariant with return type int|null of method
Psr\Http\Message\UploadedFileInterface::getSize().
131 Return type mixed of method
Grav\Framework\Form\FormFlashFile::getError() is not
covariant with return type int of method
Psr\Http\Message\UploadedFileInterface::getError().
139 Return type mixed of method
Grav\Framework\Form\FormFlashFile::getClientFilename() is
not covariant with return type string|null of method
Psr\Http\Message\UploadedFileInterface::getClientFilename
().
147 Return type mixed of method
Grav\Framework\Form\FormFlashFile::getClientMediaType()
is not covariant with return type string|null of method
Psr\Http\Message\UploadedFileInterface::getClientMediaTyp
e().
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Logger/Processors/UserProcessor.php
------ -----------------------------------------------------------
24 Parameter #1 $record (array) of method
Grav\Framework\Logger\Processors\UserProcessor::__invoke(
) is not contravariant with parameter #1 $record
(Monolog\LogRecord) of method
Monolog\Processor\ProcessorInterface::__invoke().
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Media/MediaIdentifier.php
------ -----------------------------------------------------------
30 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Media/UploadedMediaObject.php
------ -----------------------------------------------------------
36 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Mime/MimeTypes.php
------ -----------------------------------------------------------
42 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ ------------------------------------------------
Line src/Grav/Framework/Object/ObjectCollection.php
------ ------------------------------------------------
96 No error to ignore is reported on line 96.
------ ------------------------------------------------
------ ---------------------------------------------
Line src/Grav/Framework/Object/ObjectIndex.php
------ ---------------------------------------------
193 No error to ignore is reported on line 193.
------ ---------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Psr7/Stream.php
------ -----------------------------------------------------------
31 Unsafe usage of new static().
🪪 new.static
💡 See:
https://phpstan.org/blog/solving-phpstan-error-unsafe-usa
ge-of-new-static
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrai
t.php (in context of class
Grav\Framework\Psr7\ServerRequest)
------ -----------------------------------------------------------
51 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::getAttributes() is not
covariant with return type array of method
Psr\Http\Message\ServerRequestInterface::getAttributes().
60 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::getCookieParams() is
not covariant with return type array of method
Psr\Http\Message\ServerRequestInterface::getCookieParams(
).
76 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::getQueryParams() is
not covariant with return type array of method
Psr\Http\Message\ServerRequestInterface::getQueryParams()
.
84 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::getServerParams() is
not covariant with return type array of method
Psr\Http\Message\ServerRequestInterface::getServerParams(
).
92 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::getUploadedFiles() is
not covariant with return type array of method
Psr\Http\Message\ServerRequestInterface::getUploadedFiles
().
100 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withAttribute() is not
covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withAttribute().
125 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withoutAttribute() is
not covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withoutAttribute
().
136 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withCookieParams() is
not covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withCookieParams
().
147 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withParsedBody() is
not covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withParsedBody()
.
158 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withQueryParams() is
not covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withQueryParams(
).
169 Return type mixed of method
Grav\Framework\Psr7\ServerRequest::withUploadedFiles() is
not covariant with return type
Psr\Http\Message\ServerRequestInterface of method
Psr\Http\Message\ServerRequestInterface::withUploadedFile
s().
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Grav/Framework/Relationships/Relationships.php
------ -----------------------------------------------------------
107 Return type mixed of method
Grav\Framework\Relationships\Relationships::offsetSet()
is not covariant with tentative return type void of
method
ArrayAccess<string,Grav\Framework\Contracts\Relationships
\RelationshipInterface<T of Grav\Framework\Contracts\Obje
ct\IdentifierInterface, P of
Grav\Framework\Contracts\Object\IdentifierInterface>>::of
fsetSet().
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
attribute to temporarily suppress the error.
116 Return type mixed of method
Grav\Framework\Relationships\Relationships::offsetUnset()
is not covariant with tentative return type void of
method
ArrayAccess<string,Grav\Framework\Contracts\Relationships
\RelationshipInterface<T of Grav\Framework\Contracts\Obje
ct\IdentifierInterface, P of
Grav\Framework\Contracts\Object\IdentifierInterface>>::of
fsetUnset().
💡 Make it covariant, or use the #[\ReturnTypeWillChange]
attribute to temporarily suppress the error.
------ -----------------------------------------------------------
------ -----------------------------------------------------------
Line src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php
------ -----------------------------------------------------------
30 Parameter #1 $node (Twig_NodeInterface) of method
Twig\DeferredExtension\DeferredNodeVisitorCompat::enterNo
de() is not contravariant with parameter #1 $node
(Twig\Node\Node) of method
Twig\NodeVisitor\NodeVisitorInterface::enterNode().
30 Parameter $node of method
Twig\DeferredExtension\DeferredNodeVisitorCompat::enterNo
de() has invalid type Twig_NodeInterface.
🪪 class.notFound
46 Parameter #1 $node (Twig_NodeInterface) of method
Twig\DeferredExtension\DeferredNodeVisitorCompat::leaveNo
de() is not contravariant with parameter #1 $node
(Twig\Node\Node) of method
Twig\NodeVisitor\NodeVisitorInterface::leaveNode().
46 Parameter $node of method
Twig\DeferredExtension\DeferredNodeVisitorCompat::leaveNo
de() has invalid type Twig_NodeInterface.
🪪 class.notFound
------ -----------------------------------------------------------
[ERROR] Found 74 errors

View File

@@ -9,7 +9,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.50');
define('GRAV_VERSION', '1.7.50.1');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);

View File

@@ -63,21 +63,37 @@ if (is_file($quarantineFile)) {
}
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
$manifests = [];
$snapshots = [];
if (is_dir($manifestDir)) {
$files = glob($manifestDir . '/*.json');
if ($files) {
rsort($files);
foreach ($files as $file) {
$decoded = json_decode(file_get_contents($file), true);
if (is_array($decoded)) {
$decoded['file'] = basename($file);
$manifests[] = $decoded;
if (!is_array($decoded)) {
continue;
}
$id = $decoded['id'] ?? pathinfo($file, PATHINFO_FILENAME);
if (!is_string($id) || $id === '' || strncmp($id, 'snapshot-', 9) !== 0) {
continue;
}
$decoded['id'] = $id;
$decoded['file'] = basename($file);
$decoded['created_at'] = (int)($decoded['created_at'] ?? filemtime($file) ?: 0);
$snapshots[] = $decoded;
}
if ($snapshots) {
usort($snapshots, static function (array $a, array $b): int {
return ($b['created_at'] ?? 0) <=> ($a['created_at'] ?? 0);
});
}
}
}
$latestSnapshot = $snapshots[0] ?? null;
header('Content-Type: text/html; charset=utf-8');
?><!doctype html>
@@ -89,7 +105,8 @@ header('Content-Type: text/html; charset=utf-8');
<style>
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 40px; background: #111; color: #eee; }
.panel { max-width: 720px; margin: 0 auto; background: #1d1d1f; padding: 24px 32px; border-radius: 12px; box-shadow: 0 10px 45px rgba(0,0,0,0.4); }
h1 { margin-top: 0; color: #9ef; }
h1 { font-size: 2.5rem; margin-top: 0; color: #fff; display:flex;align-items:center; }
h1 > img {margin-right:1rem;}
code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
form { margin-top: 16px; }
input[type="text"] { width: 100%; padding: 10px; border: 1px solid #333; border-radius: 6px; background: #151517; color: #fff; }
@@ -106,7 +123,7 @@ header('Content-Type: text/html; charset=utf-8');
</head>
<body>
<div class="panel">
<h1>Grav Recovery Mode</h1>
<h1><img src="system/assets/grav.png">Grav Recovery Mode</h1>
<?php if ($notice): ?>
<div class="message notice"><?php echo htmlspecialchars($notice, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
@@ -153,18 +170,22 @@ header('Content-Type: text/html; charset=utf-8');
<div class="card">
<h3>Rollback</h3>
<?php if ($manifests): ?>
<?php if ($latestSnapshot): ?>
<form method="post">
<input type="hidden" name="action" value="rollback">
<label for="manifest">Choose a snapshot</label>
<select id="manifest" name="manifest">
<?php foreach ($manifests as $manifest): ?>
<option value="<?php echo htmlspecialchars($manifest['id'], ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($manifest['id'], ENT_QUOTES, 'UTF-8'); ?> — Grav <?php echo htmlspecialchars($manifest['target_version'] ?? 'unknown', ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="secondary">Rollback to Selected Snapshot</button>
<input type="hidden" name="manifest" value="<?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?>">
<p>
Latest snapshot:
<code><?php echo htmlspecialchars($latestSnapshot['id'], ENT_QUOTES, 'UTF-8'); ?></code>
<?php if (!empty($latestSnapshot['label'])): ?>
<br><small><?php echo htmlspecialchars($latestSnapshot['label'], ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
— Grav <?php echo htmlspecialchars($latestSnapshot['target_version'] ?? 'unknown', ENT_QUOTES, 'UTF-8'); ?>
<?php if (!empty($latestSnapshot['created_at'])): ?>
<br><small>Created <?php echo htmlspecialchars(date('c', (int)$latestSnapshot['created_at']), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</p>
<button type="submit" class="secondary">Rollback to Latest Snapshot</button>
</form>
<?php else: ?>
<p>No upgrade snapshots were found.</p>

View File

@@ -10,7 +10,9 @@
namespace Grav\Common\Recovery;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Yaml;
use RocketTheme\Toolbox\Event\Event;
use function bin2hex;
use function dirname;
use function file_get_contents;
@@ -32,6 +34,7 @@ use const E_COMPILE_ERROR;
use const E_CORE_ERROR;
use const E_ERROR;
use const E_PARSE;
use const E_USER_ERROR;
use const GRAV_ROOT;
use const JSON_PRETTY_PRINT;
use const JSON_UNESCAPED_SLASHES;
@@ -47,6 +50,8 @@ class RecoveryManager
private $rootPath;
/** @var string */
private $userPath;
/** @var bool */
private $failureCaptured = false;
/**
* @param mixed $context Container or root path.
@@ -77,6 +82,15 @@ class RecoveryManager
}
register_shutdown_function([$this, 'handleShutdown']);
$events = null;
try {
$events = Grav::instance()['events'] ?? null;
} catch (\Throwable $e) {
$events = null;
}
if ($events && method_exists($events, 'addListener')) {
$events->addListener('onFatalException', [$this, 'onFatalException']);
}
$this->registered = true;
}
@@ -103,6 +117,7 @@ class RecoveryManager
}
$this->closeUpgradeWindow();
$this->failureCaptured = false;
}
/**
@@ -112,38 +127,49 @@ class RecoveryManager
*/
public function handleShutdown(): void
{
if ($this->failureCaptured) {
return;
}
$error = $this->resolveLastError();
if (!$error) {
return;
}
$type = $error['type'] ?? 0;
if (!$this->isFatal($type)) {
$this->processFailure($error);
}
/**
* Handle uncaught exceptions bubbled to the top-level handler.
*
* @param \Throwable $exception
* @return void
*/
public function handleException(\Throwable $exception): void
{
if ($this->failureCaptured) {
return;
}
$file = $error['file'] ?? '';
$plugin = $this->detectPluginFromPath($file);
if (!$plugin) {
return;
}
$context = [
'created_at' => time(),
'message' => $error['message'] ?? '',
'file' => $file,
'line' => $error['line'] ?? null,
'type' => $type,
'plugin' => $plugin,
$error = [
'type' => E_ERROR,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
];
if (!$this->shouldEnterRecovery($context)) {
return;
}
$this->processFailure($error);
}
$this->activate($context);
if ($plugin) {
$this->quarantinePlugin($plugin, $context);
/**
* @param Event $event
* @return void
*/
public function onFatalException(Event $event): void
{
$exception = $event['exception'] ?? null;
if ($exception instanceof \Throwable) {
$this->handleException($exception);
}
}
@@ -175,6 +201,41 @@ class RecoveryManager
}
}
/**
* @param array $error
* @return void
*/
private function processFailure(array $error): void
{
$type = (int)($error['type'] ?? 0);
if (!$this->isFatal($type)) {
return;
}
$file = $error['file'] ?? '';
$plugin = $this->detectPluginFromPath($file);
$context = [
'created_at' => time(),
'message' => $error['message'] ?? '',
'file' => $file,
'line' => $error['line'] ?? null,
'type' => $type,
'plugin' => $plugin,
];
if (!$this->shouldEnterRecovery($context)) {
return;
}
$this->activate($context);
if ($plugin) {
$this->quarantinePlugin($plugin, $context);
}
$this->failureCaptured = true;
}
/**
* Return last recorded recovery context.
*
@@ -268,7 +329,7 @@ class RecoveryManager
*/
private function isFatal(int $type): bool
{
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true);
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_USER_ERROR], true);
}
/**
@@ -303,7 +364,7 @@ class RecoveryManager
*/
private function windowPath(): string
{
return $this->rootPath . '/system/recovery.window';
return $this->userPath . '/data/recovery.window';
}
/**
@@ -403,7 +464,10 @@ class RecoveryManager
'expires_at' => $createdAt + $ttl,
];
file_put_contents($this->windowPath(), json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$path = $this->windowPath();
Folder::create(dirname($path));
file_put_contents($path, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$this->failureCaptured = false;
}
/**

View File

@@ -64,6 +64,8 @@ class SafeUpgradeService
private $manifestStore;
/** @var \Grav\Common\Config\ConfigInterface|null */
private $config;
/** @var array|null */
private $lastManifest = null;
/** @var array */
private $ignoredDirs = [
@@ -207,6 +209,7 @@ class SafeUpgradeService
$this->reportProgress('finalizing', 'Finalizing upgrade...', null);
$this->persistManifest($manifest);
$this->lastManifest = $manifest;
$this->pruneOldSnapshots();
Folder::delete($stagePath);
@@ -246,6 +249,7 @@ class SafeUpgradeService
$manifest['mode'] = 'manual';
$this->persistManifest($manifest);
$this->lastManifest = $manifest;
$this->pruneOldSnapshots();
$this->reportProgress('complete', sprintf('Snapshot %s created.', $stageId), 100, [
@@ -409,6 +413,7 @@ class SafeUpgradeService
$this->reportProgress('rollback', 'Restoring snapshot...', null);
$this->copyEntries($entries, $backupPath, $this->rootPath, 'rollback', 'Restoring');
$this->markRollback($manifest['id']);
$this->lastManifest = $manifest;
return $manifest;
}
@@ -424,6 +429,14 @@ class SafeUpgradeService
}
}
/**
* @return array|null
*/
public function getLastManifest(): ?array
{
return $this->lastManifest;
}
/**
* @return array<string, array>
*/

View File

@@ -24,6 +24,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use ZipArchive;
use function date;
use function count;
use function is_callable;
use function strlen;
@@ -253,6 +254,44 @@ class SelfupgradeCommand extends GpmCommand
$error = 1;
} else {
$io->writeln(" '- <green>Success!</green> ");
$manifest = Install::instance()->getLastManifest();
if (is_array($manifest) && ($manifest['id'] ?? null)) {
$snapshotId = (string) $manifest['id'];
$snapshotTimestamp = isset($manifest['created_at']) ? (int) $manifest['created_at'] : null;
$manifestPath = null;
if (isset($manifest['id'])) {
$manifestPath = 'user/data/upgrades/' . $manifest['id'] . '.json';
}
$metadata = [
'scope' => 'core',
'target_version' => $remote,
'snapshot' => $snapshotId,
];
if (null !== $snapshotTimestamp) {
$metadata['snapshot_created_at'] = $snapshotTimestamp;
}
if ($manifestPath) {
$metadata['snapshot_manifest'] = $manifestPath;
}
$recovery->markUpgradeWindow('core-upgrade', $metadata);
$io->writeln(sprintf(" |- Recovery snapshot: <cyan>%s</cyan>", $snapshotId));
if (null !== $snapshotTimestamp) {
$io->writeln(sprintf(" |- Snapshot captured: <white>%s</white>", date('c', $snapshotTimestamp)));
}
if ($manifestPath) {
$io->writeln(sprintf(" |- Manifest stored at: <white>%s</white>", $manifestPath));
}
} else {
// Ensure recovery window remains active even if manifest could not be resolved.
$recovery->markUpgradeWindow('core-upgrade', [
'scope' => 'core',
'target_version' => $remote,
]);
}
$io->newLine();
$safeUpgrade->clearRecoveryFlag();
}

View File

@@ -120,6 +120,9 @@ final class Install
/** @var VersionUpdater|null */
private $updater;
/** @var array|null */
private $lastManifest = null;
/** @var static */
private static $instance;
/** @var callable|null */
@@ -268,6 +271,8 @@ ERR;
throw new RuntimeException('Oops, installer was run without prepare()!', 500);
}
$this->lastManifest = null;
try {
if (null === $this->updater) {
$versions = Versions::instance(USER_DIR . 'config/versions.yaml');
@@ -294,7 +299,8 @@ ERR;
$this->relayProgress($stage, $message, $percent);
});
}
$service->promote($this->location, $this->getVersion(), $this->ignores);
$manifest = $service->promote($this->location, $this->getVersion(), $this->ignores);
$this->lastManifest = $service->getLastManifest() ?? $manifest;
Installer::setError(Installer::OK);
} else {
Installer::install(
@@ -354,6 +360,8 @@ ERR;
$this->updater->postflight();
$this->ensureExecutablePermissions();
Cache::clearCache('all');
clearstatcache();
@@ -456,4 +464,38 @@ ERR;
// 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);
}
private function ensureExecutablePermissions(): void
{
$executables = [
'bin/grav',
'bin/plugin',
'bin/gpm',
'bin/restore',
'bin/composer.phar'
];
foreach ($executables as $relative) {
$path = GRAV_ROOT . '/' . $relative;
if (!is_file($path) || is_link($path)) {
continue;
}
$mode = @fileperms($path);
$current = $mode !== false ? ($mode & 0777) : 0644;
if (($current & 0111) === 0111) {
continue;
}
@chmod($path, $current | 0111);
}
}
/**
* @return array|null
*/
public function getLastManifest(): ?array
{
return $this->lastManifest;
}
}

View File

@@ -2,6 +2,7 @@
use Grav\Common\Filesystem\Folder;
use Grav\Common\Recovery\RecoveryManager;
use RocketTheme\Toolbox\Event\Event;
class RecoveryManagerTest extends \Codeception\TestCase\Test
{
@@ -77,6 +78,81 @@ class RecoveryManagerTest extends \Codeception\TestCase\Test
self::assertArrayHasKey('bad', $decoded);
}
public function testHandleShutdownCreatesFlagWithoutPlugin(): void
{
$manager = new class($this->tmpDir) extends RecoveryManager {
protected $error;
public function __construct(string $rootPath)
{
parent::__construct($rootPath);
$this->error = [
'type' => E_ERROR,
'file' => $this->getRootPathValue() . '/system/index.php',
'message' => 'Core failure',
'line' => 13,
];
}
protected function resolveLastError(): ?array
{
return $this->error;
}
private function getRootPathValue(): string
{
$prop = new \ReflectionProperty(RecoveryManager::class, 'rootPath');
$prop->setAccessible(true);
return $prop->getValue($this);
}
};
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
$manager->handleShutdown();
$flag = $this->tmpDir . '/user/data/recovery.flag';
self::assertFileExists($flag);
$context = json_decode(file_get_contents($flag), true);
self::assertArrayHasKey('plugin', $context);
self::assertNull($context['plugin']);
self::assertSame('Core failure', $context['message']);
$quarantine = $this->tmpDir . '/user/data/upgrades/quarantine.json';
self::assertFileDoesNotExist($quarantine);
}
public function testHandleExceptionCreatesFlag(): void
{
$manager = new RecoveryManager($this->tmpDir);
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
$manager->handleException(new \RuntimeException('Unhandled failure'));
$flag = $this->tmpDir . '/user/data/recovery.flag';
self::assertFileExists($flag);
$context = json_decode(file_get_contents($flag), true);
self::assertSame('Unhandled failure', $context['message']);
self::assertArrayHasKey('plugin', $context);
self::assertNull($context['plugin']);
$manager->clear();
}
public function testOnFatalExceptionDispatchesToHandler(): void
{
$manager = new RecoveryManager($this->tmpDir);
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
$manager->onFatalException(new Event(['exception' => new \RuntimeException('Event failure')]));
$flag = $this->tmpDir . '/user/data/recovery.flag';
self::assertFileExists($flag);
$context = json_decode(file_get_contents($flag), true);
self::assertSame('Event failure', $context['message']);
$manager->clear();
}
public function testHandleShutdownIgnoresNonFatalErrors(): void
{
$manager = new class($this->tmpDir) extends RecoveryManager {

View File

@@ -187,8 +187,11 @@ PHP;
{
[$root] = $this->prepareLiveEnvironment();
$flag = $root . '/user/data/recovery.flag';
$window = $root . '/user/data/recovery.window';
Folder::create(dirname($flag));
file_put_contents($flag, 'flag');
Folder::create(dirname($window));
file_put_contents($window, json_encode(['expires_at' => time() + 120]));
$service = new SafeUpgradeService([
'root' => $root,
@@ -196,6 +199,7 @@ PHP;
$service->clearRecoveryFlag();
self::assertFileDoesNotExist($flag);
self::assertFileExists($window);
}
/**

0
user/config/media.yaml Normal file
View File

View File

@@ -1,45 +1,242 @@
absolute_urls: false
timezone: null
param_sep: ':'
wrapped_site: false
reverse_proxy_setup: false
force_ssl: false
force_lowercase_urls: true
custom_base_url: null
username_regex: '^[a-z0-9_-]{3,16}$'
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
intl_enabled: true
http_x_forwarded:
protocol: true
host: false
port: true
ip: true
languages:
supported: null
default_lang: null
include_default_lang: true
include_default_lang_file_extension: true
translations: true
translations_fallback: true
session_store_active: false
http_accept_language: false
override_locale: false
pages_fallback_only: false
debug: false
home:
alias: '/home'
alias: /home
hide_in_urls: false
pages:
type: regular
dirs:
- 'page://'
theme: quark
markdown:
extra: false
order:
by: default
dir: asc
list:
count: 20
dateformat:
default: null
short: 'jS M Y'
long: 'F jS \a\t g:ia'
publish_dates: true
process:
markdown: true
twig: false
twig_first: false
never_cache_twig: false
events:
page: true
twig: true
markdown:
extra: false
auto_line_breaks: false
auto_url_links: false
escape_markup: false
special_chars:
'>': gt
'<': lt
valid_link_attributes:
- rel
- target
- id
- class
- classes
types:
- html
- htm
- xml
- txt
- json
- rss
- atom
append_url_extension: null
expires: 604800
cache_control: null
last_modified: false
etag: true
vary_accept_encoding: false
redirect_default_code: '302'
redirect_trailing_slash: 1
redirect_default_route: 0
ignore_files:
- .DS_Store
ignore_folders:
- .git
- .idea
ignore_hidden: true
hide_empty_folders: false
url_taxonomy_filters: true
frontmatter:
process_twig: false
ignore_fields:
- form
- forms
cache:
enabled: true
check:
method: file
driver: auto
prefix: 'g'
prefix: g
purge_at: '0 4 * * *'
clear_at: '0 3 * * *'
clear_job_type: standard
clear_images_by_default: false
cli_compatibility: false
lifetime: 604800
purge_max_age_days: 30
gzip: false
allow_webserver_gzip: false
redis:
socket: '0'
password: null
database: null
server: null
port: null
memcache:
server: null
port: null
memcached:
server: null
port: null
twig:
cache: true
debug: true
auto_reload: true
autoescape: true
undefined_functions: true
undefined_filters: true
safe_functions: { }
safe_filters: { }
umask_fix: false
assets:
css_pipeline: false
css_pipeline_include_externals: true
css_pipeline_before_excludes: true
css_minify: true
css_minify_windows: false
css_rewrite: true
js_pipeline: false
js_pipeline_include_externals: true
js_pipeline_before_excludes: true
js_module_pipeline: false
js_module_pipeline_include_externals: true
js_module_pipeline_before_excludes: true
js_minify: true
enable_asset_timestamp: false
enable_asset_sri: false
collections:
jquery: 'system://assets/jquery/jquery-3.x.min.js'
errors:
display: true
display: 1
log: true
log:
handler: file
syslog:
facility: local6
tag: grav
debugger:
enabled: false
twig: true
provider: clockwork
censored: false
shutdown:
close_connection: true
twig: true
images:
adapter: gd
default_image_quality: 85
cache_all: false
cache_perms: '0755'
debug: false
auto_fix_orientation: true
seofriendly: false
cls:
auto_sizes: false
aspect_ratio: false
retina_scale: '1'
defaults:
loading: auto
decoding: auto
fetchpriority: auto
watermark:
image: 'system://images/watermark.png'
position_y: center
position_x: center
scale: 33
watermark_all: false
media:
enable_media_timestamp: false
unsupported_inline_types: null
allowed_fallback_types: null
auto_metadata_exif: false
upload_limit: 2097152
session:
enabled: true
initialize: true
timeout: 1800
name: grav-site
uniqueness: path
secure: false
secure_https: true
httponly: true
samesite: Lax
split: true
domain: null
path: null
gpm:
releases: testing
official_gpm_only: true
verify_peer: true
updates:
safe_upgrade: true
http:
method: auto
enable_proxy: true
proxy_url: null
proxy_cert_path: null
concurrent_connections: 5
verify_peer: true
verify_host: true
accounts:
type: regular
storage: file
avatar: gravatar
flex:
cache:
index:
enabled: true
lifetime: 60
object:
enabled: true
lifetime: 600
render:
enabled: true
lifetime: 600
strict_mode:
yaml_compat: false
twig_compat: false
blueprint_compat: false