diff --git a/src/CoreBundle/Migrations/AbstractMigrationChamilo.php b/src/CoreBundle/Migrations/AbstractMigrationChamilo.php index bd8b77207d7..f0d858e0839 100644 --- a/src/CoreBundle/Migrations/AbstractMigrationChamilo.php +++ b/src/CoreBundle/Migrations/AbstractMigrationChamilo.php @@ -46,6 +46,13 @@ abstract class AbstractMigrationChamilo extends AbstractMigration private LoggerInterface $logger; + /** + * Cache to avoid repeated DB lookups for the same legacy token. + * + * @var array + */ + private array $legacyCourseExistsCache = []; + public function __construct(Connection $connection, LoggerInterface $logger) { parent::__construct($connection, $logger); @@ -556,4 +563,186 @@ protected function guessResourcePathForLog(ResourceInterface $resource): string return ''; } + + /** + * Checks whether a legacy token matches an existing course by code or directory. + */ + protected function legacyCourseExistsByCodeOrDirectory(string $token): bool + { + $token = trim($token); + if ($token === '') { + return false; + } + + if (array_key_exists($token, $this->legacyCourseExistsCache)) { + return $this->legacyCourseExistsCache[$token]; + } + + $sql = 'SELECT 1 FROM course WHERE code = :t OR directory = :t LIMIT 1'; + $exists = (bool) $this->connection->fetchOne($sql, ['t' => $token]); + + $this->legacyCourseExistsCache[$token] = $exists; + + return $exists; + } + + /** + * Quick HTML file check based on extension. + */ + protected function isHtmlFile(string $filePath): bool + { + $ext = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION)); + + return in_array($ext, ['html', 'htm'], true); + } + + /** + * Rewrites legacy "/courses/{TOKEN}/document/{REL_PATH}" links when TOKEN is not an existing course, + * falling back to the current course directory if the referenced file exists there. + */ + protected function rewriteLegacyCoursesDocumentLinksFallbackToCurrentCourse( + string $html, + string $currentCourseDirectory + ): string { + if ($html === '' || $currentCourseDirectory === '') { + return $html; + } + + // Fast pre-check. + if (false === strpos($html, '/courses/') || false === strpos($html, '/document/')) { + return $html; + } + + $updateRootPath = rtrim($this->getUpdateRootPath(), '/'); + $currentCourseDirectory = trim($currentCourseDirectory); + + $rewriteUrl = function (string $url) use ($updateRootPath, $currentCourseDirectory): string { + // Extract optional prefix (scheme+host), keep it if present. + $prefix = ''; + $rest = $url; + + if (preg_match('~^(https?:\/\/[^\/]+)(\/.*)$~i', $url, $m)) { + $prefix = (string) $m[1]; + $rest = (string) $m[2]; + } + + // Only handle /courses/{TOKEN}/document/{REL...} + if (!preg_match('~^\/courses\/([^\/]+)\/document\/(.+)$~i', $rest, $m)) { + return $url; + } + + $token = (string) $m[1]; + $relWithSuffix = (string) $m[2]; + + // If course exists, do not touch it. + if ($this->legacyCourseExistsByCodeOrDirectory($token)) { + return $url; + } + + // Split REL from query/fragment to check filesystem path. + $rel = $relWithSuffix; + $suffix = ''; + if (preg_match('~^([^?#]+)([?#].*)$~', $relWithSuffix, $mm)) { + $rel = (string) $mm[1]; + $suffix = (string) $mm[2]; + } + + $relDecoded = rawurldecode($rel); + + // Safety: avoid traversal. + if (str_contains($relDecoded, '..')) { + @error_log('[Migration][Documents] Skipping suspicious legacy link containing "..": '.$url); + return $url; + } + + $fsPath = $updateRootPath.'/app/courses/'.$currentCourseDirectory.'/document/'.$relDecoded; + if (!is_file($fsPath)) { + @error_log('[Migration][Documents] Legacy token "'.$token.'" does not exist and file not found in current course "'.$currentCourseDirectory.'": '.$url); + return $url; + } + + $newUrl = $prefix.'/courses/'.$currentCourseDirectory.'/document/'.$rel.$suffix; + @error_log('[Migration][Documents] Rewrote legacy link: '.$url.' -> '.$newUrl); + + return $newUrl; + }; + + // Pass 1: src/href="..." + $html = (string) preg_replace_callback( + '~\b(?:src|href)\s*=\s*([\'"])([^\'"]+)\1~i', + function (array $m) use ($rewriteUrl) { + $quote = (string) $m[1]; + $url = (string) $m[2]; + + if (false === strpos($url, '/courses/') || false === strpos($url, '/document/')) { + return $m[0]; + } + + $newUrl = $rewriteUrl($url); + if ($newUrl === $url) { + return $m[0]; + } + + return str_replace($url, $newUrl, $m[0]); + }, + $html + ); + + // Pass 2: url(...) + $html = (string) preg_replace_callback( + '~\burl\(\s*([\'"]?)([^\'")]+)\1\s*\)~i', + function (array $m) use ($rewriteUrl) { + $url = (string) $m[2]; + + if (false === strpos($url, '/courses/') || false === strpos($url, '/document/')) { + return $m[0]; + } + + $newUrl = $rewriteUrl($url); + if ($newUrl === $url) { + return $m[0]; + } + + return str_replace($url, $newUrl, $m[0]); + }, + $html + ); + + return $html; + } + + /** + * If HTML file contains legacy broken /courses/{TOKEN}/document links, create a rewritten temp copy and return its path. + * Otherwise returns original file path. + * + * Important: Use $originalBasename to keep the original name when uploading. + */ + protected function rewriteHtmlFileLegacyLinksIfNeeded(string $filePath, string $currentCourseDirectory): string + { + if (!$this->fileExists($filePath) || !$this->isHtmlFile($filePath)) { + return $filePath; + } + + $html = (string) @file_get_contents($filePath); + if ($html === '') { + return $filePath; + } + + $newHtml = $this->rewriteLegacyCoursesDocumentLinksFallbackToCurrentCourse($html, $currentCourseDirectory); + if ($newHtml === $html) { + return $filePath; + } + + // Write a temp copy under cache dir (same as other migration temp files). + $ext = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION)); + $tmpName = 'rewrite_html_'.sha1($filePath).'.'.$ext; + + $tmpPath = $this->container->get('kernel')->getCacheDir().'/migration_'.$tmpName; + $fs = new Filesystem(); + $fs->dumpFile($tmpPath, $newHtml); + + @error_log('[Migration][Documents] Created rewritten HTML temp file: '.$tmpPath); + + return $tmpPath; + } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php b/src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php index 746d6a8ba3c..f97ed4268d3 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php @@ -242,7 +242,21 @@ public function up(Schema $schema): void $documentPath = ltrim($documentPath, '/'); $filePath = $this->getUpdateRootPath().'/app/courses/'.$course->getDirectory().'/document/'.$documentPath; error_log('MIGRATIONS :: $filePath -- '.$filePath.' ...'); - $this->addLegacyFileToResource($filePath, $documentRepo, $document, $documentId); + + // If this is an HTML document, rewrite legacy broken links before uploading. + $filePathToUpload = $this->rewriteHtmlFileLegacyLinksIfNeeded($filePath, (string) $course->getDirectory()); + + // Preserve original filename even if we upload a temp rewritten copy. + $originalFilename = basename($filePath); + + $this->addLegacyFileToResource( + $filePathToUpload, + $documentRepo, + $document, + $documentId, + $originalFilename + ); + $this->entityManager->persist($document); if (($counter % $batchSize) === 0) {