Reference documentation for developers working on nextcloud-exelearning.
For the user-facing overview and quick start, see README.md.
For the rules coding agents must follow, see AGENTS.md.
composer install
npm install
npm run build # writes js/exelearning-main.mjs + chunks
npm run typecheck
npm testTo work on the frontend with auto-rebuild:
npm run watchTo install the app into a local Nextcloud:
ln -s "$(pwd)" /path/to/nextcloud/apps/exelearning
sudo -E -u www-data php /path/to/nextcloud/occ app:enable exelearningThe Service Worker requires https:// or a localhost origin. A plain
http:// deployment over the LAN will fail to register the worker and the
viewer will show "Service Workers are not available in this browser
context."
The repository ships pure tests that do not need a running Nextcloud instance.
# JavaScript / TypeScript
npm install
npm run typecheck # vue-tsc
npm test # vitest — 46 cases covering paths, MIME, validator,
# zip-reader, files MIME helpers# PHP — minimal unit tests without composer.
# Uses tests/bootstrap-standalone.php which stubs the OCP base classes
# so the tests can run in any PHP 8.1+ environment.
php /tmp/phpunit.phar \
--no-coverage --no-configuration \
--bootstrap=tests/bootstrap-standalone.php \
tests/UnitWith a real composer install (composer install) you can run the same
suite via vendor/bin/phpunit --configuration tests/phpunit.xml.
After opening a .elpx you should see, in the browser DevTools:
- Network — a
GET /apps/exelearning/package/by-file-id/{id}request that returnsapplication/vnd.exelearning.elpx. - Application → Service Workers — a worker registered against this
origin with scope
/apps/exelearning/runtime/. - Network (with "Other" filter on) — requests to
/apps/exelearning/runtime/{sessionId}/index.htmland to relative assets beneath that path, all served by the Service Worker (the size column shows "(ServiceWorker)"). - A sandboxed
<iframe>in the Viewer modal whosesrcmatches the runtime URL.
Nextcloud does not know about application/vnd.exelearning.elpx by default,
so existing .elpx uploads are usually detected as application/zip or
application/octet-stream. The viewer handler covers both, but a clean
admin install should also configure mapping:
config/mimetypemapping.json:
{
"elpx": ["application/vnd.exelearning.elpx", "application/zip"],
"elp": ["application/vnd.exelearning.elpx", "application/zip"]
}Both extensions get the same vendor MIME so they share the eXeLearning
icon (and the same viewer / editor flow). Legacy .elp content is
detected by the editor and migrated to .elpx on first save — see
issue #20.
Then refresh Nextcloud's MIME caches:
sudo -E -u www-data php occ maintenance:mimetype:update-js
sudo -E -u www-data php occ maintenance:mimetype:update-db --repair-filecacheDo not edit Nextcloud core mimetypemapping.dist.json directly.
The Files list normally shows the preview provided by
ElpxPreviewProvider (the package's screenshot.png, or the bundled
fallback when there is none). For contexts that bypass core/preview
— sharing dialogs, breadcrumbs, the new Vue Files app's icon column —
configure the static MIME icon too:
-
Add the alias to
config/mimetypealiases.json:{ "application/vnd.exelearning.elpx": "exelearning", "application/x-exelearning": "exelearning" } -
Copy this app's icon into Nextcloud core (the only directory
maintenance:mimetype:update-jsscans for SVGs — see the comment at the top ofcore/js/mimetypelist.js):sudo install -o www-data -g www-data -m 0644 \ /var/www/nextcloud/apps/exelearning/img/filetypes/exelearning.svg \ /var/www/nextcloud/core/img/filetypes/exelearning.svg -
Refresh the MIME caches again:
sudo -E -u www-data php occ maintenance:mimetype:update-js sudo -E -u www-data php occ maintenance:mimetype:update-db --repair-filecache
Step 2 is brittle because Nextcloud upgrades may replace core/img/;
restore the SVG after each upgrade or stage it via a theme override.
The dev stack (make up) does this automatically — see the
registering .elp(x) MIME mapping + icon alias step in the Makefile.
The app registers a Viewer handler from src/main.ts, which is loaded as a
Nextcloud init script (\OCP\Util::addInitScript) so the handler is
available before the Viewer probes for MIME associations. The handler
component lives at src/viewer/ElpxViewer.vue and orchestrates:
ElpxViewer.vue
↓
elpx/elpx-loader.ts → GET /apps/exelearning/package/by-file-id/{id}
↓
elpx/zip-reader.ts (fflate) → in-memory ZIP extraction with size/entry limits
↓
elpx/package-validator.ts → require index.html
↓
elpx/viewer-session.ts → session id + per-entry map
↓
elpx/service-worker-client.ts → POST session into src/sw/exelearning-sw.js
↓
elpx/iframe-renderer.ts → sandboxed iframe at
/apps/exelearning/runtime/{session}/index.html
The Service Worker is scoped to /apps/exelearning/runtime/ and never
intercepts other Nextcloud routes.
lib/Preview/ElpxPreviewProvider.php extracts screenshot.png from the
package and returns it as a Nextcloud preview image. content.xml is never
parsed.
When the package has no screenshot.png (or it fails to decode), the
provider falls back to the bundled img/elpx-preview-fallback.png —
a white document silhouette with the upstream eXeLearning logo —
generated from the upstream PNG by tools/gen-preview-fallback.py.
The matching MIME icon for non-preview contexts lives at
img/filetypes/exelearning.svg. Nextcloud does not auto-discover icons
in app directories; see Static .elp(x) MIME icon
for the admin steps that wire it up.
Release packages already bundle the static eXeLearning editor. A source
checkout does not (js/editor/ is never committed); to enable the "Edit with
eXeLearning" action there:
make download-editor # prebuilt release zip
# or
make build-editor # clones source and builds itThe editor is dropped into js/editor/. Reload the page; the action will
appear in the Files row menu.
Save flow:
- The editor iframe posts
SAVE_FILEwith the new bytes. EditorEmbed.vuePOSTs them to/apps/exelearning/editor/savewith the open-fileIf-MatchETag. The Save button and Ctrl/Cmd+S inside the editor both go through this path.- The PHP controller refuses the write if the file changed on disk
(
HTTP 412).
.elpxHTML never runs in the parent Nextcloud window.- The iframe uses
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-popups-to-escape-sandbox". - The server-side asset fallback refuses top-level navigations
(
Sec-Fetch-Dest: document), so package HTML only renders framed. - The Service Worker only intercepts URLs under
/apps/exelearning/runtime/. - All ZIP paths are normalized;
.., absolute paths and NUL-tainted entries are rejected. - Decompressed packages are bounded by:
maxPackageSizeMb= 250 MB compressedmaxUncompressedSizeMb= 500 MBmaxZipEntries= 5000
- The backend re-checks user, file existence, read permission, MIME, and size before serving any bytes.
content.xmlis never parsed by this app.
- Public-share preview only works when the share grants read access.
Anonymous viewing of
.elpxover a public link beyond Nextcloud's normal share rules is not implemented. - The Service Worker requires an HTTPS or
localhostorigin. - Browsers without
DecompressionStreamandcrypto.randomUUIDare not supported (modern evergreen browsers only).
This is a Nextcloud admin warning, not an app error. It appears when
Nextcloud has no writable directory in its apps_paths list, which blocks
installing or updating apps from the App Store. Two fixes:
1. Add a writable custom_apps path (recommended):
sudo -E -u www-data php occ config:system:set apps_paths 0 path --value=/var/www/html/apps
sudo -E -u www-data php occ config:system:set apps_paths 0 url --value=/apps
sudo -E -u www-data php occ config:system:set apps_paths 0 writable --value=false --type=boolean
sudo -E -u www-data php occ config:system:set apps_paths 1 path --value=/var/www/html/custom_apps
sudo -E -u www-data php occ config:system:set apps_paths 1 url --value=/custom_apps
sudo -E -u www-data php occ config:system:set apps_paths 1 writable --value=true --type=boolean
sudo chown -R www-data:www-data /var/www/html/custom_appsAdjust the absolute paths to match your install layout. The Docker
quick-start uses /var/www/html/...; a Debian/Ubuntu package install
typically uses /var/www/nextcloud/....
2. Or disable the App Store entirely (read-only deploys):
sudo -E -u www-data php occ config:system:set appstoreenabled --value=false --type=booleanThis app does not need the App Store to run — it is shipped through the filesystem — so disabling it is harmless.
The viewer needs a secure context. Use https:// or browse Nextcloud at
http://localhost. Plain http:// over a LAN address will fail.
You skipped the MIME refresh. Re-run:
sudo -E -u www-data php occ maintenance:mimetype:update-js
sudo -E -u www-data php occ maintenance:mimetype:update-db --repair-filecacheThen hard-reload the Files page. The viewer also recognises .elpx by
extension as a fallback, so existing uploads still open even before the
MIME refresh — only the icon and the default action lookup change.
The Service Worker was unregistered or restarted between the page handing over the bytes and the iframe asking for them. Close and reopen the file in the Viewer; the bytes are re-extracted on every open.
js/ is build output. Run npm install && npm run build. The Service
Worker source lives at src/sw/exelearning-sw.js and is served by
SwController, not built — it never disappears.
Either composer install (full project setup) or download a PHAR:
curl -fsSL -o /tmp/phpunit.phar https://phar.phpunit.de/phpunit-10.5.phar
php /tmp/phpunit.phar --bootstrap=tests/bootstrap-standalone.php tests/UnitSee AGENTS.md and the MVP acceptance list in the project
specification for the full checklist used during reviews.