From 72cc9163bb575ee4f0b8b00dcc86726e3ff12d75 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Fri, 31 Oct 2025 11:44:55 -0500 Subject: [PATCH] Learnpath: Render lesson-created HTML inline and enable preview from documents --- assets/vue/composables/fileUtils.js | 46 ++++++++++++++----- .../Controller/ResourceController.php | 12 ++++- .../Component/CourseCopy/CourseBuilder.php | 44 +++++++++--------- 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/assets/vue/composables/fileUtils.js b/assets/vue/composables/fileUtils.js index bd6372081d6..589b8174506 100644 --- a/assets/vue/composables/fileUtils.js +++ b/assets/vue/composables/fileUtils.js @@ -1,28 +1,52 @@ export function useFileUtils() { const isFile = (fileData) => { - return fileData.resourceNode && fileData.resourceNode.firstResourceFile + return !!fileData?.resourceNode?.firstResourceFile + } + + const safeMime = (fileData) => { + // normalize: strip params like "; charset=UTF-8" + const raw = fileData?.resourceNode?.firstResourceFile?.mimeType || "" + return String(raw).split(";")[0].trim() + } + + const fileName = (fileData) => { + // prefer originalName; fallback to node title + return fileData?.resourceNode?.firstResourceFile?.originalName || fileData?.resourceNode?.title || "" + } + + const ext = (fileData) => { + const name = fileName(fileData) + const m = /\.([A-Za-z0-9]+)$/.exec(name) + return m ? m[1].toLowerCase() : "" } const isImage = (fileData) => { - return isFile(fileData) && fileData.resourceNode.firstResourceFile.image + return isFile(fileData) && !!fileData.resourceNode.firstResourceFile.image } const isVideo = (fileData) => { - return isFile(fileData) && fileData.resourceNode.firstResourceFile.video + return isFile(fileData) && !!fileData.resourceNode.firstResourceFile.video } const isAudio = (fileData) => { - const mimeType = fileData.resourceNode.firstResourceFile.mimeType - return isFile(fileData) && mimeType.split("/")[0].toLowerCase() === "audio" + if (!isFile(fileData)) return false + const top = safeMime(fileData).split("/")[0]?.toLowerCase() || "" + return top === "audio" || !!fileData.resourceNode.firstResourceFile.audio } const isHtml = (fileData) => { - if (!isFile(fileData)) { - return false - } - const mimeType = fileData.resourceNode.firstResourceFile.mimeType || "" - const [type, sub] = mimeType.split("/") - return (type?.toLowerCase() === "text" && sub?.toLowerCase() === "html") || sub?.toLowerCase() === "html" + if (!isFile(fileData)) return false + + const mime = safeMime(fileData).toLowerCase() + const e = ext(fileData) + + // MIME-based detection + const byMime = mime.includes("text/html") || mime.includes("application/html") || mime.includes("application/xhtml") + + // Extension-based fallback when MIME is missing/wrong + const byExt = e === "html" || e === "htm" || e === "xhtml" + + return byMime || byExt } const isPreviewable = (fileData) => { diff --git a/src/CoreBundle/Controller/ResourceController.php b/src/CoreBundle/Controller/ResourceController.php index 6d9fde5d91e..670392346a3 100644 --- a/src/CoreBundle/Controller/ResourceController.php +++ b/src/CoreBundle/Controller/ResourceController.php @@ -589,13 +589,21 @@ private function processFile(Request $request, ResourceNode $resourceNode, strin $fileName = $resourceFile->getOriginalName(); $fileSize = $resourceFile->getSize(); - $mimeType = $resourceFile->getMimeType(); + $mimeType = $resourceFile->getMimeType() ?: ''; [$start, $end, $length] = $this->getRange($request, $fileSize); $resourceNodeRepo = $this->getResourceNodeRepository(); // Convert the file name to ASCII using iconv $fileName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $fileName); + // MIME normalization for HTML + $looksLikeHtmlByExt = (bool) preg_match('/\.x?html?$/i', (string) $fileName); + if ($mimeType === '' || stripos($mimeType, 'html') === false) { + if ($looksLikeHtmlByExt) { + $mimeType = 'text/html; charset=UTF-8'; + } + } + switch ($mode) { case 'download': $forceDownload = true; @@ -652,7 +660,7 @@ private function processFile(Request $request, ResourceNode $resourceNode, strin $fileName ); $response->headers->set('Content-Disposition', $disposition); - $response->headers->set('Content-Type', 'text/html'); + $response->headers->set('Content-Type', 'text/html; charset=UTF-8'); // @todo move into a function/class if ('true' === $this->getSettingsManager()->getSetting('editor.translate_html')) { diff --git a/src/CourseBundle/Component/CourseCopy/CourseBuilder.php b/src/CourseBundle/Component/CourseCopy/CourseBuilder.php index 97e945d1fe4..8de9c1fef9b 100644 --- a/src/CourseBundle/Component/CourseCopy/CourseBuilder.php +++ b/src/CourseBundle/Component/CourseCopy/CourseBuilder.php @@ -464,7 +464,7 @@ public function build( * * @param array $ids */ - private function build_learnpath_category( + public function build_learnpath_category( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -505,7 +505,7 @@ private function build_learnpath_category( * * @param array $idList */ - private function build_learnpaths( + public function build_learnpaths( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -616,7 +616,7 @@ private function build_learnpaths( /** * Export Gradebook (categories + evaluations + links). */ - private function build_gradebook( + public function build_gradebook( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity @@ -729,7 +729,7 @@ private function serializeGradebookCategory(GradebookCategory $c): array * * @param array $ids */ - private function build_works( + public function build_works( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -801,7 +801,7 @@ private function build_works( * * @param array $ids */ - private function build_attendance( + public function build_attendance( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -867,7 +867,7 @@ private function build_attendance( * * @param array $ids */ - private function build_thematic( + public function build_thematic( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -959,7 +959,7 @@ private function build_thematic( * * @param array $ids */ - private function build_wiki( + public function build_wiki( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1017,7 +1017,7 @@ private function build_wiki( * * @param array $ids */ - private function build_glossary( + public function build_glossary( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1062,7 +1062,7 @@ private function build_glossary( * * @param array $ids */ - private function build_course_descriptions( + public function build_course_descriptions( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1108,7 +1108,7 @@ private function build_course_descriptions( * * @param array $ids */ - private function build_events( + public function build_events( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1213,7 +1213,7 @@ private function build_events( * * @param array $ids */ - private function build_announcements( + public function build_announcements( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1351,7 +1351,7 @@ private function tryAddAsset(string $relPath, string $absPath, int $size = 0): v * * @return array */ - private function build_surveys( + public function build_surveys( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1444,7 +1444,7 @@ private function build_surveys( * * @param array $questionIds */ - private function build_survey_questions( + public function build_survey_questions( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1528,7 +1528,7 @@ private function build_survey_questions( * * @return array */ - private function build_quizzes( + public function build_quizzes( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1641,7 +1641,7 @@ private function safeCount(mixed $v): int * * @param array $questionIds */ - private function build_quiz_questions( + public function build_quiz_questions( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1764,7 +1764,7 @@ private function exportQuestionsWithAnswers(object $legacyCourse, array $questio /** * Export Link category as legacy item. */ - private function build_link_category(CLinkCategory $category): void + public function build_link_category(CLinkCategory $category): void { $id = (int) $category->getIid(); if ($id <= 0) { @@ -1786,7 +1786,7 @@ private function build_link_category(CLinkCategory $category): void * * @param array $ids */ - private function build_links( + public function build_links( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -1949,7 +1949,7 @@ private function makeIdFilter(array $idsFilter): Closure * Export Tool intro only for the course_homepage tool. * Prefers the session-specific intro when both (session and base) exist. */ - private function build_tool_intro( + public function build_tool_intro( object $legacyCourse, ?CourseEntity $courseEntity, ?SessionEntity $sessionEntity @@ -2020,7 +2020,7 @@ private function build_tool_intro( * * @param array $ids */ - private function build_forum_category( + public function build_forum_category( object $legacyCourse, CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -2056,7 +2056,7 @@ private function build_forum_category( * * @param array $ids */ - private function build_forums( + public function build_forums( object $legacyCourse, CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -2104,7 +2104,7 @@ private function build_forums( * * @param array $ids */ - private function build_forum_topics( + public function build_forum_topics( object $legacyCourse, CourseEntity $courseEntity, ?SessionEntity $sessionEntity, @@ -2147,7 +2147,7 @@ private function build_forum_topics( * * @param array $ids */ - private function build_forum_posts( + public function build_forum_posts( object $legacyCourse, CourseEntity $courseEntity, ?SessionEntity $sessionEntity,