Skip to content

Improve legacy package migration from mod_exeweb and mod_exescorm#89

Merged
erseco merged 7 commits into
mainfrom
improve-legacy-package-migration
Jun 22, 2026
Merged

Improve legacy package migration from mod_exeweb and mod_exescorm#89
erseco merged 7 commits into
mainfrom
improve-legacy-package-migration

Conversation

@erseco

@erseco erseco commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

mod_exelearning ships a non-destructive, site-wide tool that migrates legacy mod_exeweb and mod_exescorm activities into new eXeLearning activities (admin/migrate.php, classes/local/migration/*). The detection that decided what was migratable was fragile and inconsistent between the two source handlers:

  • exescorm_source only treated a package as migratable when its filename ended in .elpx or the SCORM zip embedded an .elpx. A package that is itself a content.xml-bearing .zip (an eXeLearning content export / IMS export stored under a non-.elpx name) was reported nosource and silently skipped.
  • exeweb_source never inspected the package contents — it returned ok whenever any file existed in the package filearea, so a package with no recoverable eXeLearning source either failed at extraction (STATUS_ERROR) or migrated as a sourceless shell.

The migratability rule is now explicit and content-based: a package is migrated only when an eXeLearning content.xml (ODE 2.0) source can be recovered from it.

Package types now covered

Package Marker Result
Native .elpx content.xml at root migrated (direct) — unchanged
eXeLearning content .zip / IMS export / web export bundling source content.xml at root migrated (direct) — newly recovered
eXeLearning SCORM export wrapping its source exactly one embedded .elpx migrated (embedded) — unchanged
Several embedded .elpx >1 .elpx skipped ambiguoussource
Legacy .elp contentv3.xml, no content.xml skipped nosource
Source-less SCORM / plain web export no content.xml, no .elpx skipped nosource
External / AICC URL / synced (exescorm) exescormtype skipped unsupported
Corrupt / unreadable archive skipped nosource (no exception)

Skipped packages lose no data: the migration never deletes the source, so the legacy activity stays intact and is reported in the preflight/result table.

Changes

  • New classes/local/migration/source/package_probe.php — a shared, content-based classifier/resolver used by both source handlers. It reads only the ZIP central directory (preflight-cheap), decides the verdict from a root content.xml (precedence) or a single safe embedded .elpx, and resolves the package to a temp path (whole archive for a direct package; only the embedded entry otherwise). It reuses the existing zip_utils path-traversal/symlink defences.
  • exescorm_source and exeweb_source now delegate detection and resolution to package_probe. exescorm_source keeps its external/aiccurl/localsyncunsupported short-circuit; exeweb_source keeps its revision-itemid resolution with the restored-backup fallback scan.
  • lang/en/exelearning.phpmigratescormnote and migratestatus_nosource now state the content.xml criterion and that legacy .elp / source-less SCORM / web exports are skipped. The existing nosource bucket is reused (no new statuses or table columns).
  • The extraction engine (package_manager::extract_stored), migration_service, grade sync, view.php and the DB schema are unchanged — the change is confined to source detection.

mod_exeweb vs mod_exescorm differences

  • mod_exeweb stores one package in the package filearea at itemid = {exeweb}.revision and wipes the area on each save; it has no grades, so the target uses the per-iDevice grade model and no grade migration runs. Detection had to inspect contents (it previously did not).
  • mod_exescorm stores its package at itemid 0, supports several exescormtypes (local, localsync, embedded = a raw .elpx, external, aiccurl), and carries grades that are copied to the target's overall grade item. External/AICC-URL and synchronized types keep no migratable static snapshot and stay unsupported. The gap was a genuine SCORM/content .zip with no embedded .elpx.

Both now share one detector, so the rule lives in exactly one place.

Testing performed

  • New tests/local/migration/package_probe_test.php covers every branch (native .elpx, content.xml zip, single/multiple embedded .elpx, source-less SCORM, legacy .elp, corrupt archive, itemid threading, blocked-verdict and unsafe-entry resolution).
  • Extended exescorm_source_test, exeweb_source_test and migration_service_test (end-to-end install of a content.xml zip resolved by exescorm_source).
  • Full plugin PHPUnit suite green on Moodle 5.0.7 / PHP 8.3 (368 tests, 1311 assertions, 0 failures/errors).
  • php -l and phpcs --standard=moodle clean on all changed/added files.

Known limitations

  • The embedded-.elpx case is validated by extension (the nested zip is not opened during the cheap preflight); the extractor's index.html guard is the backstop.
  • Source-less SCORM packages and plain web exports (no content.xml) are intentionally not preserved as static content — they cannot be reconstructed as editable eXeLearning activities. Offering such preservation would be a separate product decision.

Research notes

See research/analisis/notas/AN-015-deteccion-content-xml-migracion.md (Spanish) for the analysis of what each legacy plugin stores, the content.xml criterion and verdict table, the rationale for excluding legacy .elp/source-less packages, and the test matrix.


Moodle Playground Preview

The changes in this pull request can be previewed and tested using a Moodle Playground instance.

Preview in Moodle Playground

ℹ️ The eXeLearning editor is fetched from the shared release and unpacked into the plugin when the playground boots, so the first load may take a few extra seconds. ELPX upload, viewer and preview work normally.

erseco added 5 commits June 22, 2026 09:12
Introduce a shared classifier/resolver that decides migratability by the
eXeLearning source a stored legacy package actually carries, rather than by
filename or assumption. A package is migratable when content.xml (ODE 2.0) sits
at the archive root (a native .elpx, a content.xml zip or an IMS export) or when
it embeds exactly one safe .elpx; legacy .elp (contentv3.xml), source-less SCORM
or web exports, multiple embedded .elpx, and corrupt archives are not.

Detection reads only the ZIP central directory (preflight-cheap) and reuses the
existing zip_utils path-traversal/symlink defences. Add helper_trait builders for
the new package shapes and a unit test covering every branch.
Replace the .elpx-filename + embedded-.elpx scan with the shared content-based
probe (keeping the external/aiccurl/localsync short-circuit). A non-.elpx package
carrying content.xml at its root (an eXeLearning content zip or IMS export) is now
migratable as a direct package instead of being reported nosource; a single
embedded .elpx still resolves as before. Add tests for the content.xml zip case
and the legacy .elp exclusion.
Locate the stored package (revision itemid, with the restored-backup fallback
scan) and defer migratability to the shared content-based probe. A package with
no recoverable content.xml source is now reported nosource cleanly instead of
classifying ok and failing at extraction. Add tests for the content.xml gate.
Refine migratescormnote and migratestatus_nosource to state that activities are
migrated only when an eXeLearning content.xml source can be recovered (root
content.xml or a single embedded .elpx), and that legacy .elp and source-less
SCORM/web exports are skipped. Reuses the existing nosource bucket; no new keys.
Cover the detection gap through resolution and installation: a content.xml
package stored under a non-.elpx name is detected, resolved and installed into a
servable, graded activity.
@erseco erseco requested a review from pabloamayab June 22, 2026 08:17
@erseco erseco self-assigned this Jun 22, 2026
@codecov-commenter

codecov-commenter commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.51%. Comparing base (06c53c4) to head (ad30ba7).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
classes/local/migration/source/package_probe.php 94.28% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main      #89      +/-   ##
============================================
+ Coverage     89.49%   89.51%   +0.02%     
- Complexity      976      980       +4     
============================================
  Files            52       53       +1     
  Lines          4236     4236              
============================================
+ Hits           3791     3792       +1     
+ Misses          445      444       -1     
Flag Coverage Δ
javascript 94.03% <ø> (ø)
php 89.35% <96.07%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
PHP (server-side) 89.35% <96.07%> (+0.02%) ⬆️
JavaScript (SCORM tracker) 94.03% <ø> (ø)
Files with missing lines Coverage Δ
classes/local/migration/source/exescorm_source.php 95.00% <100.00%> (+0.97%) ⬆️
classes/local/migration/source/exeweb_source.php 100.00% <100.00%> (+1.61%) ⬆️
classes/local/migration/source/package_probe.php 94.28% <94.28%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Spanish research note documenting the package variants each legacy plugin stores,
the content.xml migratability criterion and verdict table, why legacy .elp and
source-less SCORM/web exports are excluded (no data loss; source kept), and the
test matrix.
@erseco erseco force-pushed the improve-legacy-package-migration branch from 5c40906 to 80c3990 Compare June 22, 2026 08:20
@erseco erseco merged commit 4665b64 into main Jun 22, 2026
25 checks passed
@erseco erseco deleted the improve-legacy-package-migration branch June 22, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants