From 796c61e66d5087c773686397afb0c746f9bf217d Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Sat, 18 Oct 2025 12:04:25 -0600 Subject: [PATCH] restore tool - but not curretly working --- admin.php | 16 ++ classes/plugin/Admin.php | 25 +++ classes/plugin/AdminController.php | 96 +++++++++++ classes/plugin/SafeUpgradeManager.php | 158 ++++++++++++++++++ languages/en.yaml | 19 ++- themes/grav/app/updates/safe-upgrade.js | 5 +- themes/grav/js/admin.min.js | 3 +- .../partials/tools-restore-grav.html.twig | 74 ++++++++ themes/grav/templates/tools.html.twig | 6 +- 9 files changed, 393 insertions(+), 9 deletions(-) create mode 100644 themes/grav/templates/partials/tools-restore-grav.html.twig diff --git a/admin.php b/admin.php index 305b7683..0ec5a6f7 100644 --- a/admin.php +++ b/admin.php @@ -32,6 +32,7 @@ use Grav\Plugin\Admin\Popularity; use Grav\Plugin\Admin\Router; use Grav\Plugin\Admin\Themes; use Grav\Plugin\Admin\AdminController; +use Grav\Plugin\Admin\SafeUpgradeManager; use Grav\Plugin\Admin\Twig\AdminTwigExtension; use Grav\Plugin\Admin\WhiteLabel; use Grav\Plugin\Form\Form; @@ -383,6 +384,21 @@ class AdminPlugin extends Plugin 'reports' => [['admin.super'], 'PLUGIN_ADMIN.REPORTS'], 'direct-install' => [['admin.super'], 'PLUGIN_ADMIN.DIRECT_INSTALL'], ]); + + try { + $manifestFiles = glob(GRAV_ROOT . '/user/data/upgrades/*.json') ?: []; + + if (!$manifestFiles) { + $manager = new SafeUpgradeManager(Grav::instance()); + $manifestFiles = $manager->hasSnapshots() ? [true] : []; + } + + if ($manifestFiles) { + $event['tools']['restore-grav'] = [['admin.super'], 'PLUGIN_ADMIN.RESTORE_GRAV']; + } + } catch (\Throwable $e) { + // ignore availability errors, snapshots tool will simply stay hidden + } } /** diff --git a/classes/plugin/Admin.php b/classes/plugin/Admin.php index 56958e5c..8ede98ba 100644 --- a/classes/plugin/Admin.php +++ b/classes/plugin/Admin.php @@ -341,6 +341,31 @@ class Admin return array_unique($perms); } + /** + * @return array + */ + public function safeUpgradeSnapshots(): array + { + try { + $manager = new SafeUpgradeManager(); + + return $manager->listSnapshots(); + } catch (\Throwable $e) { + return []; + } + } + + public function safeUpgradeHasSnapshots(): bool + { + try { + $manager = new SafeUpgradeManager(); + + return $manager->hasSnapshots(); + } catch (\Throwable $e) { + return false; + } + } + /** * Return the languages available in the site * diff --git a/classes/plugin/AdminController.php b/classes/plugin/AdminController.php index dad7aa68..9b0c99bf 100644 --- a/classes/plugin/AdminController.php +++ b/classes/plugin/AdminController.php @@ -933,6 +933,102 @@ class AdminController extends AdminBaseController return true; } + /** + * Restore a safe-upgrade snapshot via Tools. + * + * Route: POST /tools/restore-grav?task:safeUpgradeRestore + * + * @return bool + */ + public function taskSafeUpgradeRestore() + { + if (!$this->authorizeTask('install grav', ['admin.super'])) { + return false; + } + + $post = $this->getPost($_POST ?? []); + $snapshotId = isset($post['snapshot']) ? (string)$post['snapshot'] : ''; + + if ($snapshotId === '') { + $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error'); + $this->setRedirect('/tools/restore-grav'); + + return false; + } + + $manager = $this->getSafeUpgradeManager(); + $result = $manager->restoreSnapshot($snapshotId); + + if (($result['status'] ?? 'error') === 'success') { + $manifest = $result['manifest'] ?? []; + $version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown'; + $this->admin->setMessage( + sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS'), $snapshotId, $version), + 'info' + ); + } else { + $message = $result['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_FAILED'); + $this->admin->setMessage($message, 'error'); + } + + $this->setRedirect('/tools/restore-grav'); + + return true; + } + + /** + * Delete one or more safe-upgrade snapshots via Tools. + * + * Route: POST /tools/restore-grav?task:safeUpgradeDelete + * + * @return bool + */ + public function taskSafeUpgradeDelete() + { + if (!$this->authorizeTask('install grav', ['admin.super'])) { + return false; + } + + $post = $this->getPost($_POST ?? []); + $snapshots = $post['snapshots'] ?? []; + if (is_string($snapshots)) { + $snapshots = [$snapshots]; + } + + if (empty($snapshots)) { + $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error'); + $this->setRedirect('/tools/restore-grav'); + + return false; + } + + $manager = $this->getSafeUpgradeManager(); + $results = $manager->deleteSnapshots($snapshots); + + $success = array_filter($results, static function ($item) { + return ($item['status'] ?? 'error') === 'success'; + }); + $failed = array_filter($results, static function ($item) { + return ($item['status'] ?? 'error') !== 'success'; + }); + + if ($success) { + $this->admin->setMessage( + sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_SUCCESS'), count($success)), + 'info' + ); + } + + foreach ($failed as $entry) { + $message = $entry['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_FAILED'); + $this->admin->setMessage($message, 'error'); + } + + $this->setRedirect('/tools/restore-grav'); + + return true; + } + /** * Handles uninstalling plugins and themes * diff --git a/classes/plugin/SafeUpgradeManager.php b/classes/plugin/SafeUpgradeManager.php index 24c7cdd8..f57cc5af 100644 --- a/classes/plugin/SafeUpgradeManager.php +++ b/classes/plugin/SafeUpgradeManager.php @@ -120,6 +120,164 @@ class SafeUpgradeManager $this->setJobId(null); } + /** + * @return array + */ + public function listSnapshots(): array + { + $manifestDir = GRAV_ROOT . '/user/data/upgrades'; + if (!is_dir($manifestDir)) { + return []; + } + + $files = glob($manifestDir . '/*.json') ?: []; + rsort($files); + + $snapshots = []; + foreach ($files as $file) { + $decoded = json_decode(file_get_contents($file) ?: '', true); + if (!is_array($decoded) || empty($decoded['id'])) { + continue; + } + + $createdAt = isset($decoded['created_at']) ? (int)$decoded['created_at'] : 0; + + $snapshots[] = [ + 'id' => (string)$decoded['id'], + 'source_version' => $decoded['source_version'] ?? null, + 'target_version' => $decoded['target_version'] ?? null, + 'created_at' => $createdAt, + 'created_at_iso' => $createdAt > 0 ? date('c', $createdAt) : null, + 'backup_path' => $decoded['backup_path'] ?? null, + 'package_path' => $decoded['package_path'] ?? null, + ]; + } + + return $snapshots; + } + + public function hasSnapshots(): bool + { + return !empty($this->listSnapshots()); + } + + /** + * @param string $snapshotId + * @return array{status:string,message:?string,manifest:array|null} + */ + public function restoreSnapshot(string $snapshotId): array + { + try { + $safeUpgrade = $this->getSafeUpgradeService(); + $manifest = $safeUpgrade->rollback($snapshotId); + } catch (RuntimeException $e) { + return [ + 'status' => 'error', + 'message' => $e->getMessage(), + 'manifest' => null, + ]; + } catch (Throwable $e) { + return [ + 'status' => 'error', + 'message' => $e->getMessage(), + 'manifest' => null, + ]; + } + + if (!$manifest) { + return [ + 'status' => 'error', + 'message' => sprintf('Snapshot %s not found.', $snapshotId), + 'manifest' => null, + ]; + } + + return [ + 'status' => 'success', + 'message' => null, + 'manifest' => $manifest, + ]; + } + + /** + * @param array $snapshotIds + * @return array + */ + public function deleteSnapshots(array $snapshotIds): array + { + $ids = array_values(array_unique(array_filter(array_map('strval', $snapshotIds)))); + $results = []; + + foreach ($ids as $id) { + $results[] = $this->deleteSnapshot($id); + } + + return $results; + } + + /** + * @param string $snapshotId + * @return array{id:string,status:string,message:?string} + */ + protected function deleteSnapshot(string $snapshotId): array + { + $manifestDir = GRAV_ROOT . '/user/data/upgrades'; + $manifestPath = $manifestDir . '/' . $snapshotId . '.json'; + + if (!is_file($manifestPath)) { + return [ + 'id' => $snapshotId, + 'status' => 'error', + 'message' => sprintf('Snapshot %s not found.', $snapshotId), + ]; + } + + $manifest = json_decode(file_get_contents($manifestPath) ?: '', true); + if (!is_array($manifest)) { + return [ + 'id' => $snapshotId, + 'status' => 'error', + 'message' => sprintf('Snapshot %s manifest is corrupted.', $snapshotId), + ]; + } + + $errors = []; + foreach (['package_path', 'backup_path'] as $key) { + $path = isset($manifest[$key]) ? (string)$manifest[$key] : ''; + if ($path === '' || !file_exists($path)) { + continue; + } + + try { + if (is_dir($path)) { + Folder::delete($path); + } else { + @unlink($path); + } + } catch (Throwable $e) { + $errors[] = $e->getMessage(); + } + } + + if (!@unlink($manifestPath)) { + $errors[] = sprintf('Unable to delete manifest file %s.', $manifestPath); + } + + if ($errors) { + return [ + 'id' => $snapshotId, + 'status' => 'error', + 'message' => implode(' ', $errors), + ]; + } + + return [ + 'id' => $snapshotId, + 'status' => 'success', + 'message' => sprintf('Snapshot %s removed.', $snapshotId), + ]; + } + protected function getJobDir(string $jobId): string { return $this->jobsDir . '/' . $jobId; diff --git a/languages/en.yaml b/languages/en.yaml index 51173662..7b7431db 100644 --- a/languages/en.yaml +++ b/languages/en.yaml @@ -540,8 +540,8 @@ PLUGIN_ADMIN: SAFE_UPGRADE_STAGE_COMPLETE: "Upgrade complete" SAFE_UPGRADE_STAGE_ERROR: "Upgrade encountered an error" SAFE_UPGRADE_RESULT_SUCCESS: "Grav upgraded to v%s" - SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: %s" - SAFE_UPGRADE_RESULT_ROLLBACK: "Rollback snapshot stored at: %s" + SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: %s" + SAFE_UPGRADE_RESULT_HINT: "Restore snapshots from Tools → Restore Grav." SAFE_UPGRADE_RESULT_NOOP: "Grav is already up to date." SAFE_UPGRADE_RESULT_FAILURE: "Safe upgrade failed" OF_THIS: "of this" @@ -840,6 +840,21 @@ PLUGIN_ADMIN: TOOLS_DIRECT_INSTALL_URL_TITLE: "Install Package via Remote URL Reference" TOOLS_DIRECT_INSTALL_URL_DESC: "Alternatively, you can also reference a full URL to the package ZIP file and install it via this remote URL." TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Upload and install" + RESTORE_GRAV: "Restore Grav" + RESTORE_GRAV_DESC: "Select a snapshot created by Safe Upgrade to restore your site or remove snapshots you no longer need." + RESTORE_GRAV_TABLE_SNAPSHOT: "Snapshot" + RESTORE_GRAV_TABLE_VERSION: "Version" + RESTORE_GRAV_TABLE_CREATED: "Created" + RESTORE_GRAV_TABLE_LOCATION: "Snapshot path" + RESTORE_GRAV_TABLE_ACTIONS: "Actions" + RESTORE_GRAV_RESTORE_BUTTON: "Restore" + RESTORE_GRAV_DELETE_SELECTED: "Delete Selected" + RESTORE_GRAV_NONE: "No safe upgrade snapshots are currently available." + RESTORE_GRAV_INVALID: "Select at least one snapshot before continuing." + RESTORE_GRAV_SUCCESS: "Snapshot %s restored (Grav %s)." + RESTORE_GRAV_FAILED: "Unable to restore the selected snapshot." + RESTORE_GRAV_DELETE_SUCCESS: "%d snapshot(s) deleted." + RESTORE_GRAV_DELETE_FAILED: "Failed to delete one or more snapshots." ROUTE_OVERRIDES: "Route Overrides" ROUTE_DEFAULT: "Default Route" ROUTE_CANONICAL: "Canonical Route" diff --git a/themes/grav/app/updates/safe-upgrade.js b/themes/grav/app/updates/safe-upgrade.js index 38089375..94368fe4 100644 --- a/themes/grav/app/updates/safe-upgrade.js +++ b/themes/grav/app/updates/safe-upgrade.js @@ -859,14 +859,13 @@ export default class SafeUpgrade { if (status === 'success' || status === 'finalized') { const manifest = result.manifest || {}; const target = result.version || manifest.target_version || ''; - const backup = manifest.backup_path || ''; const identifier = manifest.id || ''; this.steps.result.html(`

${r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s')}

- ${identifier ? `

${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s')}

` : ''} - ${backup ? `

${r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s')}

` : ''} + ${identifier ? `

${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s')}

` : ''} +

${t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.')}

`); diff --git a/themes/grav/js/admin.min.js b/themes/grav/js/admin.min.js index 3675f4bb..1ed570ed 100644 --- a/themes/grav/js/admin.min.js +++ b/themes/grav/js/admin.min.js @@ -5258,9 +5258,8 @@ var SafeUpgrade = /*#__PURE__*/function () { if (status === 'success' || status === 'finalized') { var manifest = result.manifest || {}; var target = result.version || manifest.target_version || ''; - var backup = manifest.backup_path || ''; var identifier = manifest.id || ''; - this.steps.result.html("\n
\n

".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "

\n ").concat(identifier ? "

".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s'), "

") : '', "\n ").concat(backup ? "

".concat(r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s'), "

") : '', "\n
\n ")); + this.steps.result.html("\n
\n

".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "

\n ").concat(identifier ? "

".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s'), "

") : '', "\n

").concat(t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.'), "

\n
\n ")); this.switchStep('result'); external_jQuery_default()('[data-gpm-grav]').remove(); if (target) { diff --git a/themes/grav/templates/partials/tools-restore-grav.html.twig b/themes/grav/templates/partials/tools-restore-grav.html.twig new file mode 100644 index 00000000..8f557cf2 --- /dev/null +++ b/themes/grav/templates/partials/tools-restore-grav.html.twig @@ -0,0 +1,74 @@ +

{{ "PLUGIN_ADMIN.RESTORE_GRAV"|t }}

+ +
+ {% set snapshots = admin.safeUpgradeSnapshots() %} + + {% if snapshots %} +

+ {{ "PLUGIN_ADMIN.RESTORE_GRAV_DESC"|t }} +

+ +
+ + {{ nonce_field('admin-form', 'admin-nonce')|raw }} +
+ + + + + + + + + + + + + + {% for snapshot in snapshots %} + {% set version = snapshot.source_version ?: snapshot.target_version %} + + + + + + + + + {% endfor %} + +
{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_SNAPSHOT"|t }}{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_VERSION"|t }}{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_CREATED"|t }}{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_LOCATION"|t }}{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_ACTIONS"|t }}
+ + {{ snapshot.id }}{{ version ?: "PLUGIN_ADMIN.UNKNOWN"|t }} + {% if snapshot.created_at %} + {{ snapshot.created_at|date('Y-m-d H:i:s') }} + {{ snapshot.created_at|nicetime(false, false) }} + {% else %} + {{ "PLUGIN_ADMIN.UNKNOWN"|t }} + {% endif %} + + {% if snapshot.backup_path %} + {{ snapshot.backup_path }} + {% else %} + {{ "PLUGIN_ADMIN.UNKNOWN"|t }} + {% endif %} + +
+ + + {{ nonce_field('admin-form', 'admin-nonce')|raw }} + +
+
+ +
+ +
+ {% else %} +
+

{{ "PLUGIN_ADMIN.RESTORE_GRAV_NONE"|t }}

+
+ {% endif %} +
diff --git a/themes/grav/templates/tools.html.twig b/themes/grav/templates/tools.html.twig index 7706707a..51ed06fa 100644 --- a/themes/grav/templates/tools.html.twig +++ b/themes/grav/templates/tools.html.twig @@ -2,8 +2,11 @@ {% set tools_slug = uri.basename %} {% if tools_slug == 'tools' %}{% set tools_slug = 'backups' %}{% endif %} -{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %} {% set tools = admin.tools() %} +{% if tools[tools_slug] is not defined %} + {% set tools_slug = tools|keys|first %} +{% endif %} +{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %} {% set titlebar -%} {% include 'partials/tools-' ~ tools_slug ~ '-titlebar.html.twig' ignore missing %} @@ -47,4 +50,3 @@

Unauthorized

{% endif %} {% endblock %} -