Skip to content

Commit

Permalink
Render H5P files entirely on the clientside.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed Mar 28, 2021
1 parent f217631 commit 689baa5
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 171 deletions.
2 changes: 0 additions & 2 deletions kolibri/core/content/static/assets/h5p/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
This code is currently pulled verbatim from https://github.com/tunapanda/h5p-standalone/tree/master/dist

To update, copy in the files (with `frame.bundle.js` copied into the JS folder). You can delete any pre-existing files.

To make `frame.bundle.js` compatible with running it inside a sandboxed iframe replace every instance of `window.parent.H5PIntegration` with `window.H5PIntegration`.
6 changes: 3 additions & 3 deletions kolibri/core/content/static/assets/h5p/js/frame.bundle.js

Large diffs are not rendered by default.

66 changes: 4 additions & 62 deletions kolibri/core/content/templates/content/h5p.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,76 +18,18 @@
<link type="text/css" rel="stylesheet" media="all" href="{% zc_static 'assets/h5p/styles/h5p-confirmation-dialog.css' %}" />
<link type="text/css" rel="stylesheet" media="all" href="{% zc_static 'assets/h5p/styles/h5p-core-button.css' %}" />

{% for path in cssfiles %}
<link type="text/css" rel="stylesheet" media="all" href="{{ path }}" />
{% endfor %}

<script>

window.H5PIntegration = {
pathIncludesVersion: {{ path_includes_version }},
contents: {
"cid-1": {
library: "{{ library }}",
jsonContent: {{ content|safe }},
displayOptions: {
copyright: false,
download: false,
embed: false,
export: false,
frame: false,
icon: false
}
}
},
url: ".",
l10n: {
H5P: {
advancedHelp: "Include this script on your website if you want dynamic sizing of the embedded content:",
author: "Author",
by: "by",
close: "Close",
contentChanged: "This content has changed since you last used it.",
copyrightInformation: "Rights of use",
copyrights: "Rights of use",
copyrightsDescription: "View copyright information for this content.",
disableFullscreen: "Disable fullscreen",
download: "Download",
downloadDescription: "Download this content as a H5P file.",
embed: "Embed",
embedDescription: "View the embed code for this content.",
fullscreen: "Fullscreen",
h5pDescription: "Visit H5P.org to check out more cool content.",
hideAdvanced: "Hide advanced",
license: "License",
noCopyrights: "No copyright information available for this content.",
showAdvanced: "Show advanced",
showLess: "Show less",
showMore: "Show more",
size: "Size",
source: "Source",
startingOver: "You'll be starting over.",
subLevel: "Sublevel",
thumbnail: "Thumbnail",
title: "Title",
year: "Year"
}
}
};

var H5P = window.H5P || {};
H5P.externalEmbed = true;
H5P.preventInit = true;

{{ hashi_initialize|safe }}

</script>

<script type="text/javascript" src="{% zc_static 'assets/h5p/js/frame.bundle.js' %}" crossorigin="anonymous"></script>

{% for path in jsfiles %}
<script type="text/javascript" src="{{ path }}"></script>
{% endfor %}

</head>
<body>
<div class="h5p-content" data-content-id="1"/>
</body>
<body></body>
</html>
8 changes: 7 additions & 1 deletion kolibri/core/content/utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
VALID_STORAGE_FILENAME = re.compile(r"[0-9a-f]{32}(-data)?\.[0-9a-z]+")

# set of file extensions that should be considered zip files and allow access to internal files
POSSIBLE_ZIPPED_FILE_EXTENSIONS = set([".zip", ".h5p"])
POSSIBLE_ZIPPED_FILE_EXTENSIONS = set([".zip"])


def _maybe_makedirs(path):
Expand Down Expand Up @@ -252,6 +252,8 @@ def get_file_checksums_url(channel_id, baseurl, version="1"):

ZIPCONTENT = "zipcontent/"

H5P = "h5p/"


def zip_content_path_prefix():
path_prefix = conf.OPTIONS["Deployment"]["ZIP_CONTENT_URL_PATH_PREFIX"]
Expand Down Expand Up @@ -296,6 +298,10 @@ def zip_content_static_root():
return urljoin(zip_content_path_prefix(), "static/")


def get_h5p_path():
return "{}{}".format(zip_content_path_prefix(), H5P)


def get_content_storage_file_url(filename):
"""
Return the URL at which the specified file can be accessed. For regular files, this is a link to the static
Expand Down
128 changes: 29 additions & 99 deletions kolibri/core/content/zip_wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
from __future__ import print_function
from __future__ import unicode_literals

import json
import logging
import mimetypes
import os
import re
import time
import zipfile
from collections import OrderedDict

import html5lib
from cheroot import wsgi
Expand All @@ -29,8 +27,8 @@
from django.utils.encoding import force_str
from django.utils.http import http_date

from kolibri.core.content.errors import InvalidStorageFilenameError
from kolibri.core.content.utils.paths import get_content_storage_file_path
from kolibri.core.content.utils.paths import get_h5p_path
from kolibri.core.content.utils.paths import get_hashi_base_path
from kolibri.core.content.utils.paths import get_hashi_html_filename
from kolibri.core.content.utils.paths import get_hashi_js_filename
Expand Down Expand Up @@ -135,38 +133,6 @@ def hashi_view(environ, start_response):
return django_response_to_wsgi(response, environ, start_response)


def load_json_from_zipfile(zf, filepath):
with zf.open(filepath, "r") as f:
return json.load(f)


def recursive_h5p_dependencies(zf, data, prefix=""):

jsfiles = OrderedDict()
cssfiles = OrderedDict()

# load the dependencies, recursively, to extract their JS and CSS paths to include
for dep in data.get("preloadedDependencies", []):
packagepath = "{machineName}-{majorVersion}.{minorVersion}/".format(**dep)
librarypath = packagepath + "library.json"
content = load_json_from_zipfile(zf, librarypath)
newjs, newcss = recursive_h5p_dependencies(zf, content, packagepath)
cssfiles.update(newcss)
jsfiles.update(newjs)

# load the JS required for the current package
for js in data.get("preloadedJs", []):
path = prefix + js["path"]
jsfiles[path] = True

# load the CSS required for the current package
for css in data.get("preloadedCss", []):
path = prefix + css["path"]
cssfiles[path] = True

return jsfiles, cssfiles


INITIALIZE_HASHI_FROM_IFRAME = "if (window.parent && window.parent.hashi) {try {window.parent.hashi.initializeIframe(window);} catch (e) {}}"


Expand Down Expand Up @@ -229,56 +195,8 @@ def parse_html(content):
return content


def get_h5p(zf):
file_size = 0
# Get the h5p bootloader, and then run it through our hashi templating code.
# return the H5P bootloader code
try:
h5pdata = load_json_from_zipfile(zf, "h5p.json")
contentdata = load_json_from_zipfile(zf, "content/content.json")
except KeyError:
return HttpResponseNotFound("No valid h5p file was found at this location")
jsfiles, cssfiles = recursive_h5p_dependencies(zf, h5pdata)
jsfiles = jsfiles.keys()
cssfiles = cssfiles.keys()
path_includes_version = (
"true" if "-" in [name for name in zf.namelist() if "/" in name][0] else "false"
)
main_library_data = [
lib
for lib in h5pdata["preloadedDependencies"]
if lib["machineName"] == h5pdata["mainLibrary"]
][0]
bootstrap_content = h5p_template.render(
Context(
{
"jsfiles": jsfiles,
"cssfiles": cssfiles,
"content": json.dumps(
json.dumps(contentdata, separators=(",", ":"), ensure_ascii=False)
),
"library": "{machineName} {majorVersion}.{minorVersion}".format(
**main_library_data
),
"path_includes_version": path_includes_version,
}
),
)
content = parse_html(bootstrap_content)
content_type = "text/html"
response = HttpResponse(content, content_type=content_type)
file_size = len(response.content)
if file_size:
response["Content-Length"] = file_size
return response


def get_embedded_file(zipped_path, zipped_filename, embedded_filepath):
with zipfile.ZipFile(zipped_path) as zf:

# handle H5P files
if zipped_path.endswith("h5p") and not embedded_filepath:
return get_h5p(zf)
# if no path, or a directory, is being referenced, look for an index.html file
if not embedded_filepath or embedded_filepath.endswith("/"):
embedded_filepath += "index.html"
Expand All @@ -300,9 +218,7 @@ def get_embedded_file(zipped_path, zipped_filename, embedded_filepath):
content_type = (
mimetypes.guess_type(embedded_filepath)[0] or "application/octet-stream"
)
if zipped_filename.endswith("zip") and (
embedded_filepath.endswith("htm") or embedded_filepath.endswith("html")
):
if embedded_filepath.endswith("htm") or embedded_filepath.endswith("html"):
content = zf.open(info).read()
html = parse_html(content)
response = HttpResponse(html, content_type=content_type)
Expand All @@ -320,16 +236,6 @@ def get_embedded_file(zipped_path, zipped_filename, embedded_filepath):

path_regex = re.compile("/(?P<zipped_filename>[^/]+)/(?P<embedded_filepath>.*)")


def get_zipped_file_path(zipped_filename):
# calculate the local file path to the zip file
zipped_path = get_content_storage_file_path(zipped_filename)
# if the zipfile does not exist on disk, return a 404
if not os.path.exists(zipped_path):
raise InvalidStorageFilenameError()
return zipped_path


YEAR_IN_SECONDS = 60 * 60 * 24 * 365


Expand All @@ -346,9 +252,10 @@ def _zip_content_from_request(request):

zipped_filename, embedded_filepath = match.groups()

try:
zipped_path = get_zipped_file_path(zipped_filename)
except InvalidStorageFilenameError:
# calculate the local file path to the zip file
zipped_path = get_content_storage_file_path(zipped_filename)
# if the zipfile does not exist on disk, return a 404
if not os.path.exists(zipped_path):
return HttpResponseNotFound(
'"%(filename)s" is not a valid zip file' % {"filename": zipped_filename}
)
Expand Down Expand Up @@ -413,8 +320,31 @@ def zip_content_view(environ, start_response):
return django_response_to_wsgi(response, environ, start_response)


def h5p_view(environ, start_response):
"""
Handles GET requests and serves the hashi template
"""
request = WSGIRequest(environ)
path = request.path.replace(get_h5p_path(), "")
if path == "":
content = h5p_template.render(
Context(
{
"hashi_initialize": INITIALIZE_HASHI_FROM_IFRAME,
}
)
)
content_type = "text/html"
response = HttpResponse(content, content_type=content_type)
response["Content-Length"] = len(response.content)
else:
response = HttpResponseNotFound()
return django_response_to_wsgi(response, environ, start_response)


def get_application():
path_map = {
get_h5p_path(): h5p_view,
get_hashi_base_path(): hashi_view,
get_zip_content_base_path(): zip_content_view,
}
Expand Down
62 changes: 62 additions & 0 deletions packages/hashi/generateH5PMimeTypeDB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs');
const path = require('path');
const mimeDB = require('mime-db');

// Allowed file extensions for H5P from: https://h5p.org/allowed-file-extensions
const allowedFileExtensions = new Set([
'bmp',
'css',
'csv',
'diff',
'doc',
'docx',
'eof',
'gif',
'jpeg',
'jpg',
'js',
'json',
'mp3',
'mp4',
'm4a',
'odp',
'ods',
'odt',
'ogg',
'otf',
'patch',
'pdf',
'png',
'ppt',
'pptx',
'rtf',
'svg',
'swf',
'tif',
'tiff',
'ttf',
'txt',
'wav',
'webm',
'woff',
'xls',
'xlsx',
'xml',
'md',
'textile',
'vtt',
]);

const output = {};

for (let key in mimeDB) {
if (mimeDB[key].extensions) {
for (let ext of mimeDB[key].extensions) {
if (allowedFileExtensions.has(ext)) {
output[ext] = key;
}
}
}
}

fs.writeFileSync(path.join(__dirname, 'src', 'mimetypes.json'), JSON.stringify(output));

0 comments on commit 689baa5

Please sign in to comment.