From 6535cec3d4f8768bca7753f2beecccda03a4f0ac Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 18 Feb 2025 13:51:05 +0100 Subject: [PATCH] Handle symbolic links when updating the files --- classes/Progress/Backlog.php | 19 +++++++++++-- classes/Task/Update/UpdateFiles.php | 43 ++++++++++++++++++++++------- classes/Xml/ChecksumCompare.php | 31 +++++++++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/classes/Progress/Backlog.php b/classes/Progress/Backlog.php index fddf54d54..ca3d72f4c 100644 --- a/classes/Progress/Backlog.php +++ b/classes/Progress/Backlog.php @@ -37,21 +37,28 @@ class Backlog */ private $backlog; + /** + * @var array Indexed by relative path from root folder, value is the symbolic link target + */ + private $symbolicLinks; + /** * @param mixed[] $backlog + * @param array $symbolicLinks */ - public function __construct(array $backlog, int $initialTotal) + public function __construct(array $backlog, int $initialTotal, array $symbolicLinks = []) { $this->backlog = $backlog; $this->initialTotal = $initialTotal; + $this->symbolicLinks = $symbolicLinks; } /** - * @param array{'backlog':mixed[],'initialTotal':int} $contents + * @param array{'backlog':mixed[],'initialTotal':int, 'symbolicLinks':array} $contents */ public static function fromContents($contents): self { - return new self($contents['backlog'], $contents['initialTotal']); + return new self($contents['backlog'], $contents['initialTotal'], $contents['symbolicLinks'] ?? []); } /** @@ -62,6 +69,7 @@ public function dump(): array return [ 'backlog' => $this->backlog, 'initialTotal' => $this->initialTotal, + 'symbolicLinks' => $this->symbolicLinks, ]; } @@ -82,4 +90,9 @@ public function getInitialTotal(): int { return $this->initialTotal; } + + public function getSymbolicLinks(): array + { + return $this->symbolicLinks; + } } diff --git a/classes/Task/Update/UpdateFiles.php b/classes/Task/Update/UpdateFiles.php index c68601a96..067ac67dd 100644 --- a/classes/Task/Update/UpdateFiles.php +++ b/classes/Task/Update/UpdateFiles.php @@ -72,7 +72,9 @@ public function run(): int $file = $filesToUpgrade->getNext(); // Note - upgrade this file means do whatever is needed for that file to be in the final state, delete included. - if (!$this->upgradeThisFile($file)) { + $relativeFile = str_replace($this->container->getProperty(UpgradeContainer::LATEST_PATH), '', $file); + $symbolicLinkTarget = $filesToUpgrade->getSymbolicLinks()[$relativeFile] ?? null; + if (!$this->upgradeThisFile($file, $relativeFile, $symbolicLinkTarget)) { // put the file back to the begin of the list $this->next = TaskName::TASK_ERROR; $this->logger->error($this->translator->trans('Error when trying to update file %s.', [$file])); @@ -97,17 +99,12 @@ public function run(): int * upgradeThisFile. * * @param mixed $orig The absolute path to the file from the upgrade archive + * @param string $file Relative file path from root * * @throws Exception */ - public function upgradeThisFile($orig): bool + protected function upgradeThisFile($orig, string $file, ?string $symbolicLinkTarget): bool { - // translations_custom and mails_custom list are currently not used - // later, we could handle customization with some kind of diff functions - // for now, just copy $file in str_replace($this->latestRootDir,_PS_ROOT_DIR_) - - $file = str_replace($this->container->getProperty(UpgradeContainer::LATEST_PATH), '', $orig); - // The path to the file in our prestashop directory $dest = $this->destUpgradePath . $file; @@ -118,7 +115,19 @@ public function upgradeThisFile($orig): bool return true; } - if (is_dir($orig)) { + + // Special case for symbolic links + if (!empty($symbolicLinkTarget)) { + // Remove the file/folder in case it already exists + if ($this->container->getFileSystem()->exists($dest)) { + $this->container->getFileSystem()->remove($dest); + } + + symlink($symbolicLinkTarget, $dest); + $this->logger->debug($this->translator->trans('Symbolic link %s to %s.', [$file, $symbolicLinkTarget])); + + return true; + } elseif (is_dir($orig)) { // if $dest is not a directory (that can happen), just remove that file if (!is_dir($dest) && $this->container->getFileSystem()->exists($dest)) { $this->container->getFileSystem()->remove($dest); @@ -248,6 +257,20 @@ protected function warmUp(): int } } + // Prepare symbolic links list + $symbolicLinks = $this->container->getChecksumCompare()->getSymbolicLinks($state->getDestinationVersion()); + foreach ($symbolicLinks as $linkRelativePath => $linkTarget) { + if (substr($linkRelativePath, 0, 6) === '/admin') { + // Please make sure that the condition to check if the string starts with /admin stays here, because it was replacing + // admin even in the middle of a path, not deleting some files as a result. + // Also, do not use DIRECTORY_SEPARATOR, keep forward slash, because the path come from the XML standardized. + $newLinkRelativePath = '/' . $admin_dir . substr($linkRelativePath, 6); + $symbolicLinks[$newLinkRelativePath] = $linkTarget; + unset($symbolicLinks[$linkRelativePath]); + } + } + $this->logger->debug('Warmup symbolic links : ' . var_export($symbolicLinks, true)); + // Now, we get the list of files that are either new or must be modified $list_files_to_upgrade = $this->container->getFilesystemAdapter()->listFilesInDir( $newReleasePath, 'upgrade', true @@ -258,7 +281,7 @@ protected function warmUp(): int $total_files_to_upgrade = count($list_files_to_upgrade); $this->container->getFileStorage()->save( - (new Backlog($list_files_to_upgrade, $total_files_to_upgrade))->dump(), + (new Backlog($list_files_to_upgrade, $total_files_to_upgrade, $symbolicLinks))->dump(), UpgradeFileNames::FILES_TO_UPGRADE_LIST ); diff --git a/classes/Xml/ChecksumCompare.php b/classes/Xml/ChecksumCompare.php index 5a18eb117..63c4b3b0d 100644 --- a/classes/Xml/ChecksumCompare.php +++ b/classes/Xml/ChecksumCompare.php @@ -144,6 +144,17 @@ public function getTamperedFilesOnShop(string $version) return $this->fileDifferences; } + /** + * @param string $version + * @return string[] Indexed by relative path from root folder, value is the symbolic link target + */ + public function getSymbolicLinks(string $version): array + { + $checksum = $this->fileLoader->getXmlMd5File($version); + + return $this->symbolicLinksAsArray($checksum->ps_root_dir[0]); + } + public function isAuthenticPrestashopVersion(string $version): bool { return !$this->getTamperedFilesOnShop($version); @@ -259,6 +270,26 @@ protected function md5FileAsArray(SimpleXMLElement $node, string $dir = '/') return $array; } + /** + * @return array + */ + protected function symbolicLinksAsArray(SimpleXMLElement $node, string $dir = ''): array + { + $links = []; + foreach ($node as $child) { + if (is_object($child) && $child->getName() == 'dir') { + $subDir = (string) $child['name']; + $subDirLinks = $this->symbolicLinksAsArray($child, $dir . DIRECTORY_SEPARATOR . $subDir); + $links = $links + $subDirLinks; + } elseif (is_object($child) && $child->getName() == 'link') { + $linkName = (string) $child['name']; + $links[$dir . DIRECTORY_SEPARATOR . $linkName] = (string) $child; + } + } + + return $links; + } + /** populate $this->$this->file_differences with $path * in sub arrays mail, themes, translation and core items. *