This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Test and lint (required before marking done):
make lint # PHP_CodeSniffer PSR2 — must pass
make test # PHPUnit unit tests
make test-coverage # Tests + 90% coverage minimum (used in CI)Docker dev environment:
make up # Start → http://localhost:8080 (admin@example.com / PLEASE_CHANGEME)
make shell # SSH into container
make down # StopEditor build (requires Bun, not npm/yarn):
make build-editor # Build static editor from exelearning submodulePackaging:
make package VERSION=1.2.3- PSR2 standard enforced by phpcs. Run
make fixto auto-fix. - Factories are excluded from coverage requirements.
- Test stubs live in
test/Stubs/— add new stubs there when Omeka/Laminas collaborators are needed.
- Branch names:
feature/*,fix/*,hotfix/* - CI runs on push/PR: untranslated string check, lint, test-coverage (all must pass)
See the Security & Architecture section below for the full system design. Key gotchas:
- All ELPX content is served through
ContentController(proxy) — never expose/files/exelearning/directly. - API endpoints validate CSRF tokens from session — always include
csrf_keyin API calls. - The module uses Omeka event hooks (
api.hydrate.post,api.create.post,api.delete.pre,view.show.after) — checkModule.phpbefore adding new lifecycle behavior. - URL building uses
$request->getBasePath()to support playground prefix environments.
This document describes the security considerations and system architecture implemented in the ExeLearning module.
The module enables viewing and editing of eXeLearning (.elpx) files within Omeka S. The system consists of:
+------------------+ +-------------------+ +------------------+
| Admin Interface | | Content Proxy | | Editor (iframe) |
| (media-show) |---->| (ContentController)|-->| (eXeLearning) |
+------------------+ +-------------------+ +------------------+
| | |
v v v
+------------------+ +-------------------+ +------------------+
| Modal Editor | | /files/exelearning/| | postMessage API |
| (fullscreen) | | (extracted files) | | (communication) |
+------------------+ +-------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| API Controller |<-----------------------------| Bridge JS |
| (save/load) | | (import/export) |
+------------------+ +------------------+
- Original .elpx files: Stored in Omeka's standard
/files/original/directory - Extracted content: Stored in
/files/exelearning/{sha1-hash}/directories - Thumbnails: Generated and stored as custom thumbnails for media items
All iframes displaying eXeLearning content use restrictive sandbox attributes:
<iframe
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
referrerpolicy="no-referrer"
...
></iframe>Allowed capabilities:
allow-scripts: Required for interactive contentallow-popups: Some eXeLearning content may need popupsallow-popups-to-escape-sandbox: Popups can function normally
Blocked capabilities:
allow-same-origin: Prevents access to parent page cookies/storageallow-forms: Prevents form submission to external URLsallow-top-navigation: Prevents navigation of parent page
The ContentController adds strict CSP headers for HTML content:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
media-src 'self' data: blob:;
font-src 'self' data:;
frame-src 'self';
object-src 'none';
base-uri 'self'
This prevents:
- Loading external scripts (XSS mitigation)
- Connecting to external servers
- Embedding external iframes
- Using plugins (Flash, Java, etc.)
Direct access to /files/exelearning/ is blocked. All content is served through a PHP proxy (ContentController::serveAction):
Security validations:
- Hash format validation (40 hex characters - SHA1)
- Path traversal prevention (blocks
..) - File existence verification
- MIME type detection and Content-Type headers
// Hash validation
if (!preg_match('/^[a-f0-9]{40}$/', $hash)) {
return $this->notFoundAction();
}
// Path traversal prevention
if (strpos($file, '..') !== false) {
return $this->notFoundAction();
}X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
- X-Frame-Options: Prevents clickjacking by blocking external framing
- X-Content-Type-Options: Prevents MIME-sniffing attacks
The module requires nginx rules to:
- Block direct file access:
location ^~ /files/exelearning/ {
return 403;
}- Route proxy requests to PHP:
location ^~ /exelearning/content/ {
try_files $uri /index.php$is_args$args;
}API endpoints require a valid CSRF key:
$csrfKey = $data['csrf_key'] ?? $request->getQuery('csrf_key');
$session = Container::getDefaultManager()->getStorage();
if (!$session || $csrfKey !== ($session['Omeka\Csrf'] ?? null)) {
return new ApiProblemResponse(new ApiProblem(403, 'Invalid CSRF token'));
}Edit functionality requires proper permissions:
$acl = $this->getServiceLocator()->get('Omeka\Acl');
if (!$acl->userIsAllowed('Omeka\Entity\Media', 'update')) {
return new ApiProblemResponse(new ApiProblem(403, 'Permission denied'));
}Communication uses postMessage API with origin validation:
Editor to Parent:
window.parent.postMessage({
type: 'exelearning-bridge-ready'
}, window.location.origin);
window.parent.postMessage({
type: 'exelearning-save-complete',
success: true
}, window.location.origin);Parent to Editor:
iframe.contentWindow.postMessage({
type: 'exelearning-request-save'
}, '*');- User clicks "Save to Omeka" button
- Parent sends
exelearning-request-savemessage - Bridge exports ELPX from editor
- Bridge POSTs to
/api/exelearning/save/{id}with CSRF token - Server validates token, permissions, and saves file
- Bridge sends
exelearning-save-completemessage - Parent closes modal and refreshes preview
The module handles various file types within .elpx archives:
| Extension | MIME Type |
|---|---|
| .html | text/html |
| .css | text/css |
| .js | application/javascript |
| .json | application/json |
| .png | image/png |
| .jpg/.jpeg | image/jpeg |
| .gif | image/gif |
| .svg | image/svg+xml |
| .mp4 | video/mp4 |
| .webm | video/webm |
| .mp3 | audio/mpeg |
| .ogg | audio/ogg |
| .woff/.woff2 | font/woff, font/woff2 |
| .ttf | font/ttf |
| application/pdf |
- XSS via uploaded content: Mitigated by CSP headers and iframe sandboxing
- Path traversal: Mitigated by
..filtering and hash validation - CSRF attacks: Mitigated by CSRF token validation
- Unauthorized editing: Mitigated by ACL permission checks
- Clickjacking: Mitigated by X-Frame-Options header
- Direct file access: Mitigated by nginx rules blocking /files/exelearning/
- MIME sniffing: Mitigated by X-Content-Type-Options header
- Ensure nginx is properly configured with the blocking rules
- Review CSP headers if specific eXeLearning content requires external resources
- Keep the module updated for security patches
- Monitor server logs for suspicious access patterns
- Consider additional rate limiting on API endpoints