Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions src/CoreBundle/Migrations/AbstractMigrationChamilo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,bool>
*/
private array $legacyCourseExistsCache = [];

public function __construct(Connection $connection, LoggerInterface $logger)
{
parent::__construct($connection, $logger);
Expand Down Expand Up @@ -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;
}
}
16 changes: 15 additions & 1 deletion src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading