diff --git a/bin/grav b/bin/grav index 793946245..bf0ccac36 100755 --- a/bin/grav +++ b/bin/grav @@ -13,6 +13,14 @@ use Grav\Console\Application\GravApplication; \define('GRAV_CLI', true); \define('GRAV_REQUEST_TIME', microtime(true)); +// Early PHP version check before loading any Grav code +if (PHP_VERSION_ID < 80300) { + echo "Grav 1.8 requires PHP 8.3.0+. You are running PHP " . PHP_VERSION . "\n"; + echo "Your web server may use a different PHP version.\n"; + echo "Check your hosting control panel for the correct CLI PHP path.\n"; + exit(1); +} + if (!file_exists(__DIR__ . '/../vendor/autoload.php')){ // Before we can even start, we need to run composer first require_once __DIR__ . '/../system/src/Grav/Common/Composer.php'; diff --git a/composer.json b/composer.json index 9330d04b2..1fac85073 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpunit/php-code-coverage": "^11.0", - "getgrav/markdowndocs": "^2.0", + "getgrav/markdowndocs": "^3.0", "codeception/module-asserts": "*", "codeception/module-phpbrowser": "*", "rector/rector": "^2.1" @@ -134,7 +134,7 @@ ] }, "scripts": { - "api": "vendor/bin/phpdoc-md generate system/src > user/pages/17/14.api/default.md", + "api": "vendor/bin/phpdoc-md generate --html system/src > ~/workspace/grav-learn/user/pages/api/default.md", "post-create-project-cmd": "bin/grav install", "rector": "vendor/bin/rector", "rector:php-compat": "@php vendor/bin/rector process --config=system/rector.php --ansi --no-progress-bar", diff --git a/composer.lock b/composer.lock index 19b4445a2..9fe1aff12 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b39160710f90a14d6be86b6c7237404a", + "content-hash": "853eaabfa780ccf3609b909cc345eb0b", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -4902,20 +4902,21 @@ }, { "name": "getgrav/markdowndocs", - "version": "2.0.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator.git", - "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064" + "reference": "b441b225709cf08a62c21c7c5dd775c59b7aaffe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getgrav/PHP-Markdown-Documentation-Generator/zipball/4a24d1b64a88da17e8f1696dc64969f5ca769064", - "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064", + "url": "https://api.github.com/repos/getgrav/PHP-Markdown-Documentation-Generator/zipball/b441b225709cf08a62c21c7c5dd775c59b7aaffe", + "reference": "b441b225709cf08a62c21c7c5dd775c59b7aaffe", "shasum": "" }, "require": { - "php": ">=5.5.0", + "erusev/parsedown": "^1.7", + "php": ">=8.0", "symfony/console": ">=2.6" }, "require-dev": { @@ -4948,9 +4949,9 @@ "description": "Command line tool for generating markdown-formatted class documentation", "homepage": "https://github.com/victorjonsson/PHP-Markdown-Documentation-Generator", "support": { - "source": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator/tree/2.0.1" + "source": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator/tree/3.0.1" }, - "time": "2021-04-20T06:04:42+00:00" + "time": "2026-02-17T01:09:39+00:00" }, { "name": "guzzlehttp/guzzle", diff --git a/index.php b/index.php index abde8d26d..df16d9814 100644 --- a/index.php +++ b/index.php @@ -61,6 +61,19 @@ if (PHP_SAPI !== 'cli') { } } +// Maintenance mode during core upgrade +if (file_exists(__DIR__ . '/.upgrading')) { + if (time() - filemtime(__DIR__ . '/.upgrading') > 300) { + @unlink(__DIR__ . '/.upgrading'); // Stale flag (>5 min), remove it + } else { + http_response_code(503); + header('Retry-After: 60'); + echo '
Please try again in a moment.
'; + exit; + } +} + // Ensure vendor libraries exist $autoload = __DIR__ . '/vendor/autoload.php'; if (!is_file($autoload)) { diff --git a/system/config/system.yaml b/system/config/system.yaml index bb2cf700c..764f59774 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -206,7 +206,7 @@ gpm: updates: safe_upgrade: true # Enable guarded staging+rollback pipeline for Grav self-updates safe_upgrade_snapshot_limit: 5 # Maximum number of safe-upgrade snapshots to retain (0 = unlimited) - recovery_mode: true # Enable recovery mode when fatal errors occur during upgrades + recovery_mode: false # Enable recovery mode when fatal errors occur during upgrades http: method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index 2263d0c60..bed374c2e 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -298,6 +298,12 @@ class Installer */ public static function sophisticatedInstall($source_path, $install_path, $ignores = [], $keep_source = false) { + // Set maintenance mode flag and clear opcache before file operations + @file_put_contents(GRAV_ROOT . '/.upgrading', date('Y-m-d H:i:s')); + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + foreach (new DirectoryIterator($source_path) as $file) { if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores, true)) { continue; @@ -325,6 +331,13 @@ class Installer } } + // Remove maintenance mode flag and clear opcache after file operations + @unlink(GRAV_ROOT . '/.upgrading'); + clearstatcache(true); + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + return true; } diff --git a/system/src/Grav/Installer/Install.php b/system/src/Grav/Installer/Install.php index 6aa0d2503..5a1c84b1f 100644 --- a/system/src/Grav/Installer/Install.php +++ b/system/src/Grav/Installer/Install.php @@ -83,8 +83,8 @@ final class Install 'grav' => [ 'name' => 'Grav', 'versions' => [ - '1.7' => '1.7.50', - '' => '1.7.50' + '1.7' => '1.7.51', + '' => '1.7.51' ] ], 'plugins' => [ @@ -249,12 +249,25 @@ final class Install $errors = implode("';
+}
+
+out("Grav Standalone Upgrade Script", $isCli);
+out("==============================", $isCli);
+out("", $isCli);
+
+// Step 1: Validate environment
+out("[1/7] Validating environment...", $isCli);
+
+// Check PHP version
+if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
+ outError("PHP {$minPhpVersion}+ required. You are running PHP " . PHP_VERSION, $isCli);
+ if (!$isCli) {
+ out("If your web server shows a different PHP version, try running this script via CLI:", $isCli);
+ out(" php8.3 upgrade.php", $isCli);
+ }
+ exit(1);
+}
+out(" PHP " . PHP_VERSION . " — OK", $isCli);
+
+// Verify this is a Grav installation
+if (!is_file($gravRoot . '/index.php') || !is_dir($gravRoot . '/system') || !is_dir($gravRoot . '/bin')) {
+ outError("This does not appear to be a Grav installation.", $isCli);
+ outError("Place this script in your Grav root directory.", $isCli);
+ exit(1);
+}
+out(" Grav root: {$gravRoot} — OK", $isCli);
+
+// Check write permissions
+if (!is_writable($gravRoot . '/system')) {
+ outError("The system/ directory is not writable.", $isCli);
+ exit(1);
+}
+out(" Write permissions — OK", $isCli);
+
+// Read current version
+$currentVersion = 'unknown';
+$definesFile = $gravRoot . '/system/defines.php';
+if (is_file($definesFile)) {
+ $content = file_get_contents($definesFile);
+ if (preg_match("/define\('GRAV_VERSION',\s*'([^']+)'\)/", $content, $m)) {
+ $currentVersion = $m[1];
+ }
+}
+out(" Current Grav version: {$currentVersion}", $isCli);
+
+// Check if ZipArchive is available
+if (!class_exists('ZipArchive')) {
+ outError("PHP ZipArchive extension is required but not available.", $isCli);
+ exit(1);
+}
+out(" ZipArchive — OK", $isCli);
+out("", $isCli);
+
+// Step 2: Confirm upgrade
+if ($isCli) {
+ out("[2/7] About to upgrade Grav from {$currentVersion} to latest 1.8.x", $isCli);
+ out(" Press Enter to continue or Ctrl+C to abort...", $isCli);
+ if (defined('STDIN')) {
+ fgets(STDIN);
+ }
+} else {
+ // In browser mode, check for confirmation parameter
+ if (!isset($_GET['confirm']) || $_GET['confirm'] !== 'yes') {
+ echo "";
+ echo "Ready to upgrade Grav from {$currentVersion} to latest 1.8.x
"; + echo ""; + echo ""; + exit(0); + } + out("[2/7] Upgrade confirmed.", $isCli); +} +out("", $isCli); + +// Step 3: Download update package +out("[3/7] Downloading Grav update package...", $isCli); +$tmpDir = $gravRoot . '/tmp'; +if (!is_dir($tmpDir)) { + @mkdir($tmpDir, 0775, true); +} +$zipPath = $tmpDir . '/grav-upgrade-' . date('YmdHis') . '.zip'; + +$downloaded = false; + +// Try curl first +if (function_exists('curl_init')) { + $ch = curl_init($gravDownloadUrl); + $fp = fopen($zipPath, 'w'); + if ($fp) { + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 300); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + fclose($fp); + + if ($httpCode === 200 && filesize($zipPath) > 0) { + $downloaded = true; + } else { + @unlink($zipPath); + if ($error) { + out(" curl failed: {$error}", $isCli); + } + } + } +} + +// Fallback to file_get_contents +if (!$downloaded && ini_get('allow_url_fopen')) { + $context = stream_context_create([ + 'http' => [ + 'timeout' => 300, + 'follow_location' => true, + ] + ]); + $data = @file_get_contents($gravDownloadUrl, false, $context); + if ($data !== false && strlen($data) > 0) { + file_put_contents($zipPath, $data); + $downloaded = true; + } +} + +if (!$downloaded) { + outError("Failed to download update package.", $isCli); + outError("You can manually download it and place it at: {$zipPath}", $isCli); + exit(1); +} + +$sizeMb = round(filesize($zipPath) / 1048576, 1); +out(" Downloaded {$sizeMb}MB to {$zipPath}", $isCli); +out("", $isCli); + +// Step 4: Create backup of system/ and vendor/ +out("[4/7] Backing up system/ and vendor/...", $isCli); +$backupDir = $gravRoot . '/backup/pre-upgrade-' . date('YmdHis'); +@mkdir($backupDir, 0775, true); + +function recursiveCopy($src, $dst) { + if (is_link($src)) { + $target = readlink($src); + if ($target !== false) { + @symlink($target, $dst); + } + return; + } + + if (is_dir($src)) { + @mkdir($dst, 0775, true); + $files = scandir($src); + foreach ($files as $file) { + if ($file === '.' || $file === '..') continue; + recursiveCopy($src . '/' . $file, $dst . '/' . $file); + } + } else { + @copy($src, $dst); + } +} + +recursiveCopy($gravRoot . '/system', $backupDir . '/system'); +out(" Backed up system/", $isCli); +recursiveCopy($gravRoot . '/vendor', $backupDir . '/vendor'); +out(" Backed up vendor/", $isCli); + +// Also backup key root files +foreach (['index.php', 'composer.json', 'composer.lock'] as $rootFile) { + if (is_file($gravRoot . '/' . $rootFile)) { + @copy($gravRoot . '/' . $rootFile, $backupDir . '/' . $rootFile); + } +} +out(" Backed up root files (index.php, composer.json, composer.lock)", $isCli); +out(" Backup location: {$backupDir}", $isCli); +out("", $isCli); + +// Step 5: Set maintenance mode and clear opcache +out("[5/7] Setting maintenance mode...", $isCli); +@file_put_contents($gravRoot . '/.upgrading', date('Y-m-d H:i:s')); +if (function_exists('opcache_reset')) { + @opcache_reset(); + out(" OPcache cleared", $isCli); +} +out(" Maintenance mode enabled", $isCli); +out("", $isCli); + +// Step 6: Extract update +out("[6/7] Extracting update package...", $isCli); +$zip = new ZipArchive(); +$result = $zip->open($zipPath); +if ($result !== true) { + outError("Failed to open zip file (error code: {$result})", $isCli); + @unlink($gravRoot . '/.upgrading'); + exit(1); +} + +$extractDir = $tmpDir . '/grav-extract-' . uniqid(); +@mkdir($extractDir, 0775, true); +$zip->extractTo($extractDir); +$zip->close(); + +// Find the extracted package root (usually grav-update/ or grav/) +$extractedRoot = null; +$entries = scandir($extractDir); +foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') continue; + if (is_dir($extractDir . '/' . $entry)) { + $extractedRoot = $extractDir . '/' . $entry; + break; + } +} + +if (!$extractedRoot || !is_file($extractedRoot . '/system/defines.php')) { + outError("Invalid update package structure.", $isCli); + @unlink($gravRoot . '/.upgrading'); + exit(1); +} + +// Read target version +$targetVersion = 'unknown'; +$targetDefines = file_get_contents($extractedRoot . '/system/defines.php'); +if (preg_match("/define\('GRAV_VERSION',\s*'([^']+)'\)/", $targetDefines, $m)) { + $targetVersion = $m[1]; +} +out(" Target version: {$targetVersion}", $isCli); + +// Items to skip during upgrade (user data, caches, etc.) +$ignores = ['backup', 'cache', 'images', 'logs', 'tmp', 'user', '.htaccess', 'robots.txt']; + +// Copy files from extracted package to Grav root +$sourceEntries = scandir($extractedRoot); +$copied = 0; +foreach ($sourceEntries as $entry) { + if ($entry === '.' || $entry === '..' || in_array($entry, $ignores)) continue; + + $src = $extractedRoot . '/' . $entry; + $dst = $gravRoot . '/' . $entry; + + if (is_dir($src)) { + // Remove old directory and copy new one + function recursiveDelete($dir) { + if (!is_dir($dir)) return; + $files = scandir($dir); + foreach ($files as $file) { + if ($file === '.' || $file === '..') continue; + $path = $dir . '/' . $file; + if (is_dir($path) && !is_link($path)) { + recursiveDelete($path); + } else { + @unlink($path); + } + } + @rmdir($dir); + } + + if (is_dir($dst) && !is_link($dst)) { + recursiveDelete($dst); + } + recursiveCopy($src, $dst); + + // Set executable permissions on bin files + if ($entry === 'bin') { + $binFiles = glob($dst . '/*'); + if ($binFiles) { + foreach ($binFiles as $binFile) { + @chmod($binFile, 0755); + } + } + } + } else { + @unlink($dst); + @copy($src, $dst); + } + $copied++; +} +out(" Copied {$copied} items", $isCli); +out("", $isCli); + +// Step 7: Cleanup and finalize +out("[7/7] Finalizing...", $isCli); + +// Remove maintenance mode +@unlink($gravRoot . '/.upgrading'); +out(" Maintenance mode disabled", $isCli); + +// Clear opcache +if (function_exists('opcache_reset')) { + @opcache_reset(); + out(" OPcache cleared", $isCli); +} + +// Clear Grav cache +$cacheDir = $gravRoot . '/cache'; +if (is_dir($cacheDir)) { + $cacheDirs = glob($cacheDir . '/*'); + if ($cacheDirs) { + foreach ($cacheDirs as $dir) { + if (is_dir($dir) && !is_link($dir) && basename($dir) !== '.gitkeep') { + recursiveDelete($dir); + } + } + } + out(" Cache cleared", $isCli); +} + +// Clean up temp files +@unlink($zipPath); +if (is_dir($extractDir)) { + recursiveDelete($extractDir); +} +out(" Temporary files cleaned up", $isCli); + +// Clear stat cache +clearstatcache(true); + +out("", $isCli); +outSuccess("Upgrade complete! Grav {$currentVersion} -> {$targetVersion}", $isCli); +out("", $isCli); +out("Backup saved to: {$backupDir}", $isCli); +out("", $isCli); +out("IMPORTANT: Delete this upgrade.php file from your Grav root for security.", $isCli); + +if (!$isCli) { + echo "