Merge pull request #1167 from pateljannat/scorm-cloud
refactor: scorm package render
This commit is contained in:
@@ -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 [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user