Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: scorm package render #1167

Merged
merged 1 commit into from
Dec 6, 2024
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
114 changes: 59 additions & 55 deletions frontend/src/pages/SCORMChapter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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,
Expand Down Expand Up @@ -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 [
{
Expand Down
3 changes: 3 additions & 0 deletions lms/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,14 @@
"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
Expand Down
46 changes: 42 additions & 4 deletions lms/lms/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
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 _
Expand All @@ -15,6 +17,7 @@
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()
Expand Down Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
19 changes: 18 additions & 1 deletion lms/page_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +15,8 @@
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):
Expand Down Expand Up @@ -138,3 +141,17 @@ def render(self):
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
Loading