Merge pull request #1167 from pateljannat/scorm-cloud

refactor: scorm package render
This commit is contained in:
Jannat Patel
2024-12-06 10:55:08 +05:30
committed by GitHub
4 changed files with 122 additions and 60 deletions

View File

@@ -61,41 +61,28 @@ const props = defineProps({
onBeforeMount(() => { onBeforeMount(() => {
sidebarStore.isSidebarCollapsed = true sidebarStore.isSidebarCollapsed = true
window.API_1484_11 = { setupSCORMAPI()
Initialize: () => 'true', })
Terminate: () => 'true',
GetValue: (key) => {
console.log(`GET: ${key}`)
return getDataFromLMS(key)
},
SetValue: (key, value) => {
console.log(`SET: ${key} to value: ${value}`)
saveDataToLMS(key, value) const chapter = createDocumentResource({
return 'true' doctype: 'Course Chapter',
}, name: props.chapterName,
Commit: () => 'true', auto: true,
GetLastError: () => '0', cache: ['chapter', props.chapterName],
GetErrorString: () => '', onSuccess(data) {
GetDiagnostic: () => '', progress.submit()
} },
window.API = { })
LMSInitialize: () => 'true',
LMSFinish: () => 'true', const enrollment = createListResource({
LMSGetValue: (key) => { doctype: 'LMS Enrollment',
console.log(`GET: ${key}`) fields: ['member', 'course'],
return getDataFromLMS(key) filters: {
}, course: props.courseName,
LMSSetValue: (key, value) => { member: user.data?.name,
console.log(`SET: ${key} to value: ${value}`) },
saveDataToLMS(key, value) auto: true,
return 'true' cache: ['enrollments', props.courseName, user.data?.name],
},
LMSCommit: () => 'true',
LMSGetLastError: () => '0',
LMSGetErrorString: () => '',
LMSGetDiagnostic: () => '',
}
}) })
const getDataFromLMS = (key) => { const getDataFromLMS = (key) => {
@@ -114,27 +101,6 @@ const saveDataToLMS = (key, value) => {
} }
} }
const enrollment = createListResource({
doctype: 'LMS Enrollment',
fields: ['member', 'course'],
filters: {
course: props.courseName,
member: user.data?.name,
},
auto: true,
cache: ['enrollments', props.courseName, user.data?.name],
})
const chapter = createDocumentResource({
doctype: 'Course Chapter',
name: props.chapterName,
auto: true,
cache: ['chapter', props.chapterName],
onSuccess(data) {
progress.submit()
},
})
const saveProgress = () => { const saveProgress = () => {
call('lms.lms.doctype.course_lesson.course_lesson.save_progress', { call('lms.lms.doctype.course_lesson.course_lesson.save_progress', {
lesson: chapter.doc.lessons[0].lesson, lesson: chapter.doc.lessons[0].lesson,
@@ -175,6 +141,44 @@ const enrollStudent = () => {
) )
} }
const setupSCORMAPI = () => {
window.API_1484_11 = {
Initialize: () => 'true',
Terminate: () => 'true',
GetValue: (key) => {
console.log(`GET: ${key}`)
return getDataFromLMS(key)
},
SetValue: (key, value) => {
console.log(`SET: ${key} to value: ${value}`)
saveDataToLMS(key, value)
return 'true'
},
Commit: () => 'true',
GetLastError: () => '0',
GetErrorString: () => '',
GetDiagnostic: () => '',
}
window.API = {
LMSInitialize: () => 'true',
LMSFinish: () => 'true',
LMSGetValue: (key) => {
console.log(`GET: ${key}`)
return getDataFromLMS(key)
},
LMSSetValue: (key, value) => {
console.log(`SET: ${key} to value: ${value}`)
saveDataToLMS(key, value)
return 'true'
},
LMSCommit: () => 'true',
LMSGetLastError: () => '0',
LMSGetErrorString: () => '',
LMSGetDiagnostic: () => '',
}
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
return [ return [
{ {

View File

@@ -220,11 +220,14 @@ lms_markdown_macro_renderers = {
"PDF": "lms.plugins.pdf_renderer", "PDF": "lms.plugins.pdf_renderer",
} }
website_path_resolver = "lms.lms.api.resolve_scorm_path"
# page_renderer to manage profile pages # page_renderer to manage profile pages
page_renderer = [ page_renderer = [
"lms.page_renderers.ProfileRedirectPage", "lms.page_renderers.ProfileRedirectPage",
"lms.page_renderers.ProfilePage", "lms.page_renderers.ProfilePage",
"lms.page_renderers.CoursePage", "lms.page_renderers.CoursePage",
"lms.page_renderers.SCORMRenderer",
] ]
# set this to "/" to have profiles on the top-level # set this to "/" to have profiles on the top-level

View File

@@ -5,7 +5,9 @@ import json
import frappe import frappe
import zipfile import zipfile
import os import os
import re
import shutil import shutil
import requests
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from frappe.translate import get_all_translations from frappe.translate import get_all_translations
from frappe import _ from frappe import _
@@ -15,6 +17,7 @@ from frappe.utils import time_diff, now_datetime, get_datetime, flt
from typing import Optional from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count from lms.lms.utils import get_average_rating, get_lesson_count
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from frappe.website.path_resolver import resolve_path as original_resolve_path
@frappe.whitelist() @frappe.whitelist()
@@ -590,7 +593,7 @@ def get_categories(doctype, filters):
def get_members(start=0, search=""): def get_members(start=0, search=""):
"""Get members for the given search term and start index. """Get members for the given search term and start index.
Args: start (int): Start index for the query. Args: start (int): Start index for the query.
search (str): Search term to filter the results. search (str): Search term to filter the results.
Returns: List of members. Returns: List of members.
""" """
@@ -919,12 +922,37 @@ def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
def extract_package(course, title, scorm_package): def extract_package(course, title, scorm_package):
package = frappe.get_doc("File", scorm_package.name) package = frappe.get_doc("File", scorm_package.name)
zip_path = package.get_full_path() zip_path = package.get_full_path()
# check_for_malicious_code(zip_path)
extract_path = frappe.get_site_path("public", "files", "scorm", course, title) extract_path = frappe.get_site_path("public", "scorm", course, title)
zipfile.ZipFile(zip_path).extractall(extract_path) zipfile.ZipFile(zip_path).extractall(extract_path)
return extract_path return extract_path
def check_for_malicious_code(zip_path):
suspicious_patterns = [
# Unsafe inline JavaScript
r'on(click|load|mouseover|error|submit|focus|blur|change|keyup|keydown|keypress|resize)=".*?"', # Inline event handlers (e.g., onerror, onclick)
r'<script.*?src=["\']http', # External script tags
r"eval\(", # Usage of eval()
r"Function\(", # Usage of Function constructor
r"(btoa|atob)\(", # Base64 encoding/decoding
# Dangerous XML patterns
r"<!ENTITY", # XXE-related
r"<\?xml-stylesheet .*?>", # External stylesheets in XML
]
with zipfile.ZipFile(zip_path, "r") as zf:
for file_name in zf.namelist():
if file_name.endswith((".html", ".js", ".xml")):
with zf.open(file_name) as file:
content = file.read().decode("utf-8", errors="ignore")
for pattern in suspicious_patterns:
if re.search(pattern, content):
frappe.throw(
_("Suspicious pattern found in {0}: {1}").format(file_name, pattern)
)
def get_manifest_file(extract_path): def get_manifest_file(extract_path):
manifest_file = None manifest_file = None
for root, dirs, files in os.walk(extract_path): for root, dirs, files in os.walk(extract_path):
@@ -999,6 +1027,16 @@ def delete_chapter(chapter):
def delete_scorm_package(scorm_package_path): def delete_scorm_package(scorm_package_path):
scorm_package_path = frappe.get_site_path("public", scorm_package_path) scorm_package_path = frappe.get_site_path("public", scorm_package_path[1:])
if os.path.exists(scorm_package_path): if os.path.exists(scorm_package_path):
shutil.rmtree(scorm_package_path) shutil.rmtree(scorm_package_path)
def resolve_scorm_path(path):
try:
if "scorm/" in path and path.endswith(".html"):
return path
except Exception:
pass
return original_resolve_path(path)

View File

@@ -3,7 +3,8 @@
Handles rendering of profile pages. Handles rendering of profile pages.
""" """
import re import re
import os
import mimetypes
import frappe import frappe
from frappe.website.page_renderers.base_renderer import BaseRenderer from frappe.website.page_renderers.base_renderer import BaseRenderer
from frappe.website.page_renderers.document_page import DocumentPage from frappe.website.page_renderers.document_page import DocumentPage
@@ -14,6 +15,8 @@ from frappe.website.page_renderers.redirect_page import RedirectPage
from frappe.website.page_renderers.static_page import StaticPage from frappe.website.page_renderers.static_page import StaticPage
from frappe.website.page_renderers.template_page import TemplatePage from frappe.website.page_renderers.template_page import TemplatePage
from frappe.website.page_renderers.web_form import WebFormPage from frappe.website.page_renderers.web_form import WebFormPage
from werkzeug.wrappers import Response
from werkzeug.wsgi import wrap_file
def get_profile_url(username): def get_profile_url(username):
@@ -138,3 +141,17 @@ class CoursePage(BaseRenderer):
else: else:
frappe.flags.redirect_location = "/lms/courses" frappe.flags.redirect_location = "/lms/courses"
return RedirectPage(self.path).render() return RedirectPage(self.path).render()
class SCORMRenderer(BaseRenderer):
def can_render(self):
return "scorm/" in self.path
def render(self):
path = os.path.join(frappe.local.site_path, "public", self.path.lstrip("/"))
f = open(path, "rb")
response = Response(
wrap_file(frappe.local.request.environ, f), direct_passthrough=True
)
response.mimetype = mimetypes.guess_type(path)[0]
return response