refactor: scorm package render

This commit is contained in:
Jannat Patel
2024-12-05 23:17:49 +05:30
parent 3134ef6392
commit 2416777df2
4 changed files with 122 additions and 60 deletions

View File

@@ -61,41 +61,28 @@ const props = defineProps({
onBeforeMount(() => {
sidebarStore.isSidebarCollapsed = true
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}`)
setupSCORMAPI()
})
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 chapter = createDocumentResource({
doctype: 'Course Chapter',
name: props.chapterName,
auto: true,
cache: ['chapter', props.chapterName],
onSuccess(data) {
progress.submit()
},
})
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 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 = () => {
call('lms.lms.doctype.course_lesson.course_lesson.save_progress', {
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(() => {
return [
{

View File

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

View File

@@ -5,7 +5,9 @@ import json
import frappe
import zipfile
import os
import re
import shutil
import requests
import xml.etree.ElementTree as ET
from frappe.translate import get_all_translations
from frappe import _
@@ -15,6 +17,7 @@ from frappe.utils import time_diff, now_datetime, get_datetime, flt
from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count
from xml.dom.minidom import parseString
from frappe.website.path_resolver import resolve_path as original_resolve_path
@frappe.whitelist()
@@ -590,7 +593,7 @@ def get_categories(doctype, filters):
def get_members(start=0, search=""):
"""Get members for the given search term and start index.
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.
"""
@@ -919,12 +922,37 @@ def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
def extract_package(course, title, scorm_package):
package = frappe.get_doc("File", scorm_package.name)
zip_path = package.get_full_path()
extract_path = frappe.get_site_path("public", "files", "scorm", course, title)
# check_for_malicious_code(zip_path)
extract_path = frappe.get_site_path("public", "scorm", course, title)
zipfile.ZipFile(zip_path).extractall(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):
manifest_file = None
for root, dirs, files in os.walk(extract_path):
@@ -999,6 +1027,16 @@ def delete_chapter(chapter):
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):
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.
"""
import re
import os
import mimetypes
import frappe
from frappe.website.page_renderers.base_renderer import BaseRenderer
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.template_page import TemplatePage
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):
@@ -138,3 +141,17 @@ class CoursePage(BaseRenderer):
else:
frappe.flags.redirect_location = "/lms/courses"
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