diff --git a/classes/plugin/SafeUpgradeManager.php b/classes/plugin/SafeUpgradeManager.php index ef7e09a1..c29a310a 100644 --- a/classes/plugin/SafeUpgradeManager.php +++ b/classes/plugin/SafeUpgradeManager.php @@ -290,6 +290,7 @@ class SafeUpgradeManager $result = [ 'job' => $manifest ?: null, 'progress' => $progress, + 'context' => $this->buildStatusContext(), ]; $this->clearJobContext(); @@ -417,6 +418,7 @@ class SafeUpgradeManager 'log' => $logPath, 'progress' => $this->getProgress(), 'job' => $this->readManifest(), + 'context' => $this->buildStatusContext(), ]; } @@ -553,6 +555,7 @@ class SafeUpgradeManager 'status' => 'noop', 'version' => $localVersion, 'message' => 'Grav is already up to date.', + 'context' => $this->buildStatusContext(), ]; } @@ -647,6 +650,7 @@ class SafeUpgradeManager 'version' => $remoteVersion, 'manifest' => $manifest, 'previous_version' => $localVersion, + 'context' => $this->buildStatusContext(), ]; } @@ -896,6 +900,7 @@ class SafeUpgradeManager 'status' => 'finalized', 'version' => $localVersion, 'message' => 'Post-install scripts completed.', + 'context' => $this->buildStatusContext(), ]; } @@ -1011,9 +1016,31 @@ class SafeUpgradeManager return [ 'status' => 'error', 'message' => $message, + 'context' => $this->buildStatusContext(), ] + $extra; } + protected function buildStatusContext(): ?string + { + $context = []; + + if ($this->jobManifestPath) { + $context['manifest'] = $this->jobManifestPath; + } + + if ($this->progressPath) { + $context['progress'] = $this->progressPath; + } + + if (!$context) { + return null; + } + + $encoded = json_encode($context); + + return $encoded === false ? null : base64_encode($encoded); + } + protected function ensureExecutablePermissions(): void { $executables = [ diff --git a/safe-upgrade-status.php b/safe-upgrade-status.php index 47bda6ab..02a15f89 100644 --- a/safe-upgrade-status.php +++ b/safe-upgrade-status.php @@ -33,11 +33,73 @@ $readJson = static function (string $path): ?array { $progress = null; $manifest = null; +$manifestPath = null; +$progressPath = null; + +$normalizeDir = static function (string $path): string { + $normalized = str_replace('\\', '/', $path); + + return rtrim($normalized, '/'); +}; + +$jobsDirNormalized = $normalizeDir($jobsDir); +$userDataDirNormalized = $normalizeDir(dirname($jobsDir)); + +$contextParam = $_GET['context'] ?? ''; +if ($contextParam !== '') { + $decodedRaw = base64_decode(strtr($contextParam, ' ', '+'), true); + if ($decodedRaw !== false) { + $decoded = json_decode($decodedRaw, true); + if (is_array($decoded)) { + $validatePath = static function (string $candidate) use ($normalizeDir, $jobsDirNormalized, $userDataDirNormalized) { + $candidate = str_replace('\\', '/', $candidate); + $directory = dirname($candidate); + $real = realpath($directory); + if ($real === false) { + return null; + } + + $real = $normalizeDir($real); + if (strpos($real, $jobsDirNormalized) !== 0 && strpos($real, $userDataDirNormalized) !== 0) { + return null; + } + + return $candidate; + }; + + if (!empty($decoded['manifest'])) { + $candidate = $validatePath((string)$decoded['manifest']); + if ($candidate) { + $manifestPath = $candidate; + if (is_file($candidate)) { + $manifest = $readJson($candidate); + } + } + } + + if (!empty($decoded['progress'])) { + $candidate = $validatePath((string)$decoded['progress']); + if ($candidate) { + $progressPath = $candidate; + if (is_file($candidate)) { + $progress = $readJson($candidate); + } + } + } + } + } +} if ($jobId !== '') { $jobPath = $jobsDir . '/' . $jobId; - $progress = $readJson($jobPath . '/progress.json'); - $manifest = $readJson($jobPath . '/manifest.json'); + $progressPath = $progressPath ?: ($jobPath . '/progress.json'); + $manifestPath = $manifestPath ?: ($jobPath . '/manifest.json'); + if (is_file($progressPath)) { + $progress = $readJson($progressPath); + } + if (is_file($manifestPath)) { + $manifest = $readJson($manifestPath); + } if (!$progress && !$manifest && !is_dir($jobPath)) { $progress = $readJson($fallbackProgress) ?: [ @@ -46,37 +108,45 @@ if ($jobId !== '') { 'percent' => null, 'timestamp' => time(), ]; - - echo json_encode([ - 'status' => 'success', - 'message' => 'Safe upgrade job not found.', - 'data' => [ - 'job' => null, - 'progress' => $progress, - ], - ]); - exit; } } if ($progress === null) { - $progress = $readJson($fallbackProgress) ?: [ - 'stage' => 'idle', - 'message' => '', - 'percent' => null, - 'timestamp' => time(), - ]; + if ($progressPath && is_file($progressPath)) { + $progress = $readJson($progressPath); + } + + if ($progress === null) { + $progress = $readJson($fallbackProgress) ?: [ + 'stage' => 'idle', + 'message' => '', + 'percent' => null, + 'timestamp' => time(), + ]; + $progressPath = $fallbackProgress; + } } if ($jobId !== '' && is_array($progress) && !isset($progress['job_id'])) { $progress['job_id'] = $jobId; } +$contextPayload = []; +if ($manifestPath) { + $contextPayload['manifest'] = $manifestPath; +} +if ($progressPath) { + $contextPayload['progress'] = $progressPath; +} + +$contextToken = $contextPayload ? base64_encode(json_encode($contextPayload)) : null; + echo json_encode([ 'status' => 'success', 'data' => [ 'job' => $manifest ?: null, 'progress' => $progress, + 'context' => $contextToken, ], ]); diff --git a/themes/grav/app/updates/safe-upgrade.js b/themes/grav/app/updates/safe-upgrade.js index fb4434f5..908e2b73 100644 --- a/themes/grav/app/updates/safe-upgrade.js +++ b/themes/grav/app/updates/safe-upgrade.js @@ -54,6 +54,7 @@ export default class SafeUpgrade { this.active = false; this.jobId = null; this.statusFailures = 0; + this.statusContext = null; this.directStatusUrl = this.resolveDirectStatusUrl(); this.preferDirectStatus = !!this.directStatusUrl; @@ -120,6 +121,7 @@ export default class SafeUpgrade { this.decisions = {}; this.statusFailures = 0; this.preferDirectStatus = !!this.directStatusUrl; + this.statusContext = null; this.renderLoading(); this.modal.open(); this.fetchPreflight(); @@ -412,6 +414,7 @@ export default class SafeUpgrade { } this.statusFailures = 0; this.preferDirectStatus = !!this.directStatusUrl; + this.statusContext = data.context || null; this.beginPolling(1200); } else { this.renderResult(data); @@ -453,8 +456,18 @@ export default class SafeUpgrade { resolveStatusEndpoint() { const useDirect = this.directStatusUrl && this.preferDirectStatus; let url = useDirect ? this.directStatusUrl : this.urls.status; + const params = []; + if (this.jobId) { - url += (url.includes('?') ? '&' : '?') + `job=${encodeURIComponent(this.jobId)}`; + params.push(`job=${encodeURIComponent(this.jobId)}`); + } + + if (this.statusContext) { + params.push(`context=${encodeURIComponent(this.statusContext)}`); + } + + if (params.length) { + url += (url.includes('?') ? '&' : '?') + params.join('&'); } return { @@ -532,6 +545,9 @@ export default class SafeUpgrade { } const payload = response.data || {}; + if (Object.prototype.hasOwnProperty.call(payload, 'context')) { + this.statusContext = payload.context || null; + } const job = payload.job || {}; const data = payload.progress || payload; nextStage = data.stage || null; diff --git a/themes/grav/js/admin.min.js b/themes/grav/js/admin.min.js index 4af30d52..f215847a 100644 --- a/themes/grav/js/admin.min.js +++ b/themes/grav/js/admin.min.js @@ -4610,6 +4610,7 @@ var SafeUpgrade = /*#__PURE__*/function () { this.active = false; this.jobId = null; this.statusFailures = 0; + this.statusContext = null; this.directStatusUrl = this.resolveDirectStatusUrl(); this.preferDirectStatus = !!this.directStatusUrl; this.registerEvents(); @@ -4676,6 +4677,7 @@ var SafeUpgrade = /*#__PURE__*/function () { this.decisions = {}; this.statusFailures = 0; this.preferDirectStatus = !!this.directStatusUrl; + this.statusContext = null; this.renderLoading(); this.modal.open(); this.fetchPreflight(); @@ -4882,6 +4884,7 @@ var SafeUpgrade = /*#__PURE__*/function () { } _this4.statusFailures = 0; _this4.preferDirectStatus = !!_this4.directStatusUrl; + _this4.statusContext = data.context || null; _this4.beginPolling(1200); } else { _this4.renderResult(data); @@ -4921,8 +4924,15 @@ var SafeUpgrade = /*#__PURE__*/function () { value: function resolveStatusEndpoint() { var useDirect = this.directStatusUrl && this.preferDirectStatus; var url = useDirect ? this.directStatusUrl : this.urls.status; + var params = []; if (this.jobId) { - url += (url.indexOf('?') !== -1 ? '&' : '?') + "job=".concat(encodeURIComponent(this.jobId)); + params.push("job=".concat(encodeURIComponent(this.jobId))); + } + if (this.statusContext) { + params.push("context=".concat(encodeURIComponent(this.statusContext))); + } + if (params.length) { + url += (url.indexOf('?') !== -1 ? '&' : '?') + params.join('&'); } return { url: url, @@ -5002,6 +5012,9 @@ var SafeUpgrade = /*#__PURE__*/function () { return; } var payload = response.data || {}; + if (Object.prototype.hasOwnProperty.call(payload, 'context')) { + _this6.statusContext = payload.context || null; + } var job = payload.job || {}; var data = payload.progress || payload; nextStage = data.stage || null;