diff --git a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php index 026a9d07f..fbbee6714 100644 --- a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php +++ b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php @@ -39,6 +39,8 @@ use function uniqid; use function trim; use function strpos; use function unlink; +use function ltrim; +use function preg_replace; use const GRAV_ROOT; use const GLOB_ONLYDIR; use const JSON_PRETTY_PRINT; @@ -75,15 +77,29 @@ class SafeUpgradeService $root = $options['root'] ?? GRAV_ROOT; $this->rootPath = rtrim($root, DIRECTORY_SEPARATOR); $this->parentDir = $options['parent_dir'] ?? dirname($this->rootPath); - $defaultStaging = $options['staging_root'] ?? ($this->parentDir . DIRECTORY_SEPARATOR . 'grav-upgrades'); - if (!$this->ensureWritable($defaultStaging)) { - $fallback = getenv('HOME') ? getenv('HOME') . DIRECTORY_SEPARATOR . 'grav-upgrades' : sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'grav-upgrades'; - if (!$this->ensureWritable($fallback)) { - throw new RuntimeException(sprintf('Unable to create staging directory at %s or fallback %s. Adjust permissions or configure system.updates.staging_root.', $defaultStaging, $fallback)); - } - $defaultStaging = $fallback; + + $candidates = []; + if (!empty($options['staging_root'])) { + $candidates[] = $options['staging_root']; + } + $candidates[] = $this->parentDir . DIRECTORY_SEPARATOR . 'grav-upgrades'; + if (getenv('HOME')) { + $candidates[] = getenv('HOME') . DIRECTORY_SEPARATOR . 'grav-upgrades'; + } + $candidates[] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'grav-upgrades'; + + $this->stagingRoot = null; + foreach ($candidates as $candidate) { + $resolved = $this->resolveStagingPath($candidate); + if ($resolved) { + $this->stagingRoot = $resolved; + break; + } + } + + if (null === $this->stagingRoot) { + throw new RuntimeException('Unable to locate writable staging directory. Configure system.updates.staging_root or adjust permissions.'); } - $this->stagingRoot = realpath($defaultStaging) ?: $defaultStaging; $this->manifestStore = $options['manifest_store'] ?? ($this->rootPath . DIRECTORY_SEPARATOR . 'user' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'upgrades'); if (isset($options['ignored_dirs']) && is_array($options['ignored_dirs'])) { $this->ignoredDirs = $options['ignored_dirs']; @@ -509,19 +525,54 @@ class SafeUpgradeService * @param string $path * @return bool */ - private function ensureWritable(string $path): bool + private function resolveStagingPath(?string $path): ?string { + if (null === $path || $path === '') { + return null; + } + + $expanded = $path; + if (0 === strpos($expanded, '~')) { + $home = getenv('HOME'); + if ($home) { + $expanded = rtrim($home, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($expanded, '~\/'); + } + } + if (!$this->isAbsolutePath($expanded)) { + $expanded = $this->rootPath . DIRECTORY_SEPARATOR . ltrim($expanded, DIRECTORY_SEPARATOR); + } + + $expanded = $this->normalizePath($expanded); + try { - Folder::create($path); + Folder::create($expanded); } catch (\RuntimeException $e) { + return null; + } + + return is_writable($expanded) ? $expanded : null; + } + + private function isAbsolutePath(string $path): bool + { + if ($path === '') { return false; } - if (!is_writable($path)) { - return false; + if ($path[0] === '/' || $path[0] === '\\') { + return true; } - return true; + return (bool)preg_match('#^[A-Za-z]:[\\/]#', $path); + } + + private function normalizePath(string $path): string + { + $path = str_replace('\\', '/', $path); + $path = preg_replace('#/+#', '/', $path); + $path = rtrim($path, '/'); + + return str_replace('/', DIRECTORY_SEPARATOR, $path); } /** diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index 99f863adc..e2eac0acf 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -278,7 +278,18 @@ class SelfupgradeCommand extends GpmCommand */ protected function createSafeUpgradeService(): SafeUpgradeService { - return new SafeUpgradeService(); + $config = null; + try { + $config = Grav::instance()['config'] ?? null; + } catch (\Throwable $e) { + $config = null; + } + + $stagingRoot = $config ? $config->get('system.updates.staging_root') : null; + + return new SafeUpgradeService([ + 'staging_root' => $stagingRoot, + ]); } /** diff --git a/system/src/Grav/Installer/Install.php b/system/src/Grav/Installer/Install.php index 02aee48b7..794e6f67e 100644 --- a/system/src/Grav/Installer/Install.php +++ b/system/src/Grav/Installer/Install.php @@ -262,7 +262,17 @@ ERR; $this->updater->install(); if ($this->shouldUseSafeUpgrade()) { - $service = new SafeUpgradeService(); + $options = []; + try { + $grav = Grav::instance(); + if ($grav && isset($grav['config'])) { + $options['staging_root'] = $grav['config']->get('system.updates.staging_root'); + } + } catch (\Throwable $e) { + // ignore + } + + $service = new SafeUpgradeService($options); $service->promote($this->location, $this->getVersion(), $this->ignores); Installer::setError(Installer::OK); } else {