Parser for eXeLearning project files with support for legacy contentv3.xml projects and modern ODE content.xml packages.
Features | Installation | Usage
ELPParser supports:
- Legacy
.elpprojects from eXeLearning 2.x based oncontentv3.xml - Modern
.elp/.elpxprojects based oncontent.xmland ODE 2.0 - eXeLearning 3 and 4 ELPX package conventions without treating them as different XML format versions
- Explicit and heuristic eXeLearning major-version detection with detection details
- Normalized metadata, strings, pages, blocks, iDevices and asset references
- Normalized iDevice state across standard JSON, DataGame, embedded JSON and HTML-only storage patterns
- Asset discovery in HTML, CSS-like values,
srcsetand structuredjsonProperties - Archive-backed asset normalization and orphan-asset detection
- Safe ZIP extraction with path traversal and symlink checks
- Configurable limits for entry count, decompressed sizes, XML size and compression ratio
- Streaming extraction to avoid loading large assets into memory
- JSON serialization
For more information, visit the documentation.
- PHP 8.0+
- Composer
ext-zipext-simplexml
Composer declares these extensions and will report a missing requirement during installation.
composer require exelearning/elp-parserProjects can be parsed from PHP streams or in-memory bytes. Stream input is copied in chunks to a bounded temporary file because ZipArchive requires a filesystem path:
$stream = fopen($_FILES['project']['tmp_name'], 'rb');
$parser = ELPParser::fromStream($stream, 'elpx');
$parserFromBytes = ELPParser::fromContents($bytes, 'elpx');Temporary files owned by parser instances are removed automatically. inspectStream() and inspectContents() provide the corresponding lightweight inspection APIs.
For cataloging or indexing, inspect() reads archive metadata and the project XML without normalizing pages, iDevices or assets:
$info = ELPParser::inspect('/path/to/project.elpx');
echo $info['title'];
echo $info['formatVersion'];
echo $info['packageProfile'];use Exelearning\ELPParser;
$parser = ELPParser::fromFile('/path/to/project.elpx');
echo $parser->getTitle();
echo $parser->getVersion();
foreach ($parser->getStrings() as $string) {
echo $string . "\n";
}echo $parser->getSourceExtension(); // elp | elpx
echo $parser->getContentFormat(); // legacy-contentv3 | ode-content
echo $parser->getContentFile(); // contentv3.xml | content.xml
echo $parser->getContentSchemaVersion(); // 2.0 for modern ODE packages
echo $parser->getFormatFamily(); // legacy | ode
echo $parser->getFormatVersion(); // null | 2.0
echo $parser->getExeVersion(); // raw upstream version string when present
echo $parser->getApplicationVersion(); // alias with explicit application semantics
echo $parser->getPackageProfile(); // legacy-v2 | elpx-v3 | elpx-v4 | ode-v3 ...
echo $parser->getResourceLayout(); // none | content-resources | legacy-temp-paths | mixed
echo $parser->getResourceProfile(); // v3-uuid-resources | v4-resource-tree | mixed-modern-resources | ...
$versionInfo = $parser->getVersionInfo();
// declared, declaredMajor, detectedMajor, source, signalsgetVersion() remains the compatibility API for the detected major version. getVersionInfo() makes it explicit whether that result came from package metadata, the package format, a heuristic, or a default.
$pages = $parser->getPages();
$visiblePages = $parser->getVisiblePages();
$blocks = $parser->getBlocks();
$idevices = $parser->getIdevices();
// Modern iDevices expose storagePattern, data and stateDecodeError.
$pageTexts = $parser->getPageTexts();
$assets = $parser->getAssets();
$assetsDetailed = $parser->getAssetsDetailed();
$orphanAssets = $parser->getOrphanAssets();
$missingAssets = $parser->getMissingAssets();
$brokenReferences = $parser->getBrokenReferences();
$internalLinks = $parser->getInternalLinks();
$brokenInternalLinks = $parser->getBrokenInternalLinks();
$manifest = $parser->getPackageManifest();
$metadata = $parser->getMetadata();
$userPreferences = $parser->getUserPreferences();
$odeResources = $parser->getOdeResources();
$odeProperties = $parser->getOdeProperties();
$pageTree = $parser->getPageTree();Direct lookup helpers are also available: getPageById(), getBlockById(), getIdeviceById(), getProjectId() and getProjectVersionId().
A parallel typed API is available through $parser->getProject(). It returns Project, Page, Block, Idevice, Asset and VersionInfo model objects while the existing array APIs remain unchanged.
Asset references are normalized against the actual ZIP entries. This prevents external URLs and nonexistent paths from being reported as package assets.
Normal parsing remains tolerant. Validation can be requested explicitly:
$result = $parser->validate();
if (!$result['valid']) {
print_r($result['errors']);
}
print_r($result['warnings']);The validator reports unresolved assets, broken internal exe-node: links, duplicate identifiers, broken page-parent relationships, hierarchy cycles, relationship/order inconsistencies, missing iDevice runtime directories and missing v4 baseline files/directories.
Schema validation is optional and only uses a caller-supplied trusted local schema:
use Exelearning\Validation\SchemaValidator;
$xsdResult = $parser->validateSchema('/trusted/path/ode-content.xsd');
$dtdResult = $parser->validateSchema(
'/trusted/path/content.dtd',
SchemaValidator::TYPE_DTD
);Package-supplied DTDs are not trusted for this API. Schema loading uses LIBXML_NONET. The optional schema-validation API requires ext-dom.
Default limits are intentionally generous but bounded. They can be overridden for trusted or unusually large packages:
use Exelearning\Archive\ArchiveLimits;
use Exelearning\ELPParser;
$limits = new ArchiveLimits(
maxEntries: 30000,
maxEntryBytes: 1073741824,
maxTotalBytes: 2147483647,
maxXmlBytes: 134217728,
maxCompressionRatio: 1000.0
);
$parser = ELPParser::fromFile('/path/to/project.elpx', $limits);The defaults are 20,000 entries, 1 GiB per entry, approximately 2 GiB total uncompressed data, 64 MiB for the project XML, and a maximum compression ratio of 1000:1.
Exact archive bytes and normalized logical content use separate fingerprints:
$archiveHash = $parser->getArchiveFingerprint();
$contentHash = $parser->getContentFingerprint();
$same = $parser->hasSameContentAs($otherParser);
$diff = $parser->diff($otherParser);The normalized content fingerprint ignores volatile package identity/version fields such as odeId, odeVersionId and the eXeLearning application version. It keeps parsed project structure and includes hashes of project resource bytes. This makes it suitable for change detection without treating ZIP packaging differences as content changes.
The semantic diff reports metadata, page, block, iDevice and resource additions/removals/changes.
$json = $parser->exportJson();
$parser->exportJson('/path/to/output.json');
$detailedJson = $parser->exportDetailedJson();
$detailed = $parser->toDetailedArray();The existing JSON export remains compact. The detailed representation adds format/version information, metadata, ODE preferences/resources/properties, pages and page tree, blocks, iDevices, assets and archive entries.
$parser->extract('/path/to/destination');Extraction is streamed entry by entry. Unsafe paths, ZIP symlinks and extraction targets that resolve outside the destination root are rejected.
Parser errors derive from Exelearning\Exception\ElpParserException. More specific exceptions include:
InvalidArchiveExceptionInvalidXmlExceptionUnsupportedFormatExceptionUnsafeArchiveExceptionResourceLimitException
use Exelearning\ELPParser;
use Exelearning\Exception\ElpParserException;
try {
$parser = ELPParser::fromFile('/path/to/project.elpx');
} catch (ElpParserException $exception) {
echo $exception->getMessage();
}The parser distinguishes the internal project format from the detected eXeLearning application/package version:
- legacy
contentv3.xmlprojects report major version2 - eXeLearning 3 and 4 use the same modern ODE
content.xmlformat with rootversion="2.0" - modern packages accept both the historical
eXeVersionresource key and the currentexe_versionkey .elpx+content.xml+ a rootcontent.dtdremains the current signal for likely v4-style packages when embedded metadata still reports3.0- v3 UUID asset directories and the v4 resource-tree layout are reported separately through
getResourceProfile() - multi-digit future major versions such as
10.xcan be parsed from version metadata
Use getFormatVersion() for the ODE format version, getApplicationVersion() for the declared eXeLearning version, and getVersionInfo() when the distinction between declared and inferred versions matters.
Parsed projects are indexed by page, block and iDevice ID for constant-time lookup. Aggregate collections and diagnostics are cached because parser instances are immutable after construction. Asset-reference resolution also caches normalized archive lookups.
The upstream compatibility corpus records per-project timings, total elapsed time, peak memory and the five slowest projects. These measurements are informational and do not impose brittle timing thresholds in CI.
The regular test suite includes a deterministic corpus for malformed XML, encoded and Unicode asset paths, malformed iDevice state and cyclic page hierarchies.
A separate Upstream Compatibility workflow runs weekly against project fixtures from exelearning/exelearning. It discovers ZIP-backed .elp / .elpx fixtures containing content.xml or contentv3.xml, compares lightweight inspection with full parsing, and fails on compatibility regressions.
The corpus runner can also be used locally:
php tests/upstream-compat.php /path/to/exelearning/test/fixturesThe project is distributed under the MIT License. See LICENSE.md.