From b25aa36201c8384efc82b0fad1151eaacafcd010 Mon Sep 17 00:00:00 2001 From: erseco Date: Thu, 14 May 2026 18:50:28 +0100 Subject: [PATCH 1/4] feat: scope file-list icons to .elp/.elpx and give legacy files the old logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #21. Two-part fix: 1. The eXeLearning icon was leaking onto every `.zip` in Nextcloud Files. Root cause: `PermissionService::isElpxFile()` accepted `application/zip` and `application/octet-stream` as standalone MIME signals, so `ElpxPreviewProvider::isAvailable()` returned `true` for any plain ZIP and the provider's fallback PNG (the eXeLearning doc outline) was rendered as the thumbnail. Split `Application::ALLOWED_MIME_TYPES` into two lists: a broad one (still includes zip/octet-stream — used by routing code that already knows the request is for an `.elpx`) and a new narrow `VENDOR_MIME_TYPES` (only `application/vnd.exelearning.elpx`, `application/x-exelearning`, and the new `application/x-exelearning-legacy`). `isElpxFile()` now uses the narrow list for MIME matching, while extension matching is unchanged — `.elpx` and `.elp` still win without a MIME check. 2. Modern `.elpx` and legacy `.elp` are now visually distinguishable. New MIME `application/x-exelearning-legacy` is registered for `.elp` via `make up`'s `mimetypemapping.json`, and a new `mimetypealiases.json` aliases the two vendor MIMEs to their respective icons: - `application/vnd.exelearning.elpx` → `exelearning` (the existing modern teal doc-with-brand-mark icon). - `application/x-exelearning-legacy` → `exelearning-legacy` (new asset: cream/olive doc shape with the pre-`.elpx` upstream logo — the green "eXe" circles over the dark globe-X — embedded as base64 PNG so the icon is a single self-contained file). `application/zip` stays out of the alias file on purpose — that was the original symptom. `ElpxPreviewProvider::MIME_REGEX` also widens to cover `x-exelearning-legacy` so the preview generator still runs on legacy archives that happen to contain a `screenshot.png`. Tests: extended `ApplicationConstantsTest` with two new assertions that pin the bug down — `VENDOR_MIME_TYPES` must not contain `application/zip` or `application/octet-stream`, and must contain both the modern and legacy vendor MIMEs. 10/10 PHPUnit, 51/51 JS, typecheck/lint/build all green. --- Makefile | 30 ++++++++++++------- img/filetypes/exelearning-legacy.svg | 15 ++++++++++ lib/AppInfo/Application.php | 30 ++++++++++++++++++- lib/Preview/ElpxPreviewProvider.php | 11 +++++-- lib/Service/PermissionService.php | 6 +++- .../Unit/AppInfo/ApplicationConstantsTest.php | 22 ++++++++++++++ 6 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 img/filetypes/exelearning-legacy.svg diff --git a/Makefile b/Makefile index 8ea37e8..b0bf1ac 100644 --- a/Makefile +++ b/Makefile @@ -399,21 +399,31 @@ up: check-docker @docker exec -u www-data $(DOCKER_NAME) php occ config:system:set apps_paths 1 writable --value=true --type=boolean >/dev/null @echo ">> enabling exelearning" @docker exec -u www-data $(DOCKER_NAME) php occ app:enable exelearning - @# `.elpx` extension → MIME mapping. This is the only piece a - @# Nextcloud app cannot ship by itself: the file is read from - @# /var/www/html/config/. See README → "Custom MIME types" for the - @# manual admin steps required on production installs. - @echo ">> registering .elpx MIME mapping" + @# `.elp(x)` → MIME mapping + icon aliases. These are the only + @# pieces a Nextcloud app cannot ship by itself: both files are + @# read from /var/www/html/config/. See README → "Custom MIME types" + @# for the manual admin steps required on production installs. + @# + @# `.elpx` files get the modern vendor MIME; `.elp` files get the + @# legacy MIME so the Files app can render the older logo for them + @# (issue #21). `application/zip` stays out of the alias file on + @# purpose — aliasing it would tag every plain ZIP with our icon. + @echo ">> registering .elp(x) MIME mapping + icon aliases" @docker exec $(DOCKER_NAME) bash -c \ - 'echo "{\"elpx\":[\"application/vnd.exelearning.elpx\",\"application/zip\"]}" \ + 'echo "{\"elpx\":[\"application/vnd.exelearning.elpx\",\"application/zip\"], \"elp\":[\"application/x-exelearning-legacy\",\"application/zip\"]}" \ > /var/www/html/config/mimetypemapping.json && \ chown www-data:www-data /var/www/html/config/mimetypemapping.json' + @docker exec $(DOCKER_NAME) bash -c \ + 'echo "{\"application/vnd.exelearning.elpx\":\"exelearning\", \"application/x-exelearning\":\"exelearning\", \"application/x-exelearning-legacy\":\"exelearning-legacy\"}" \ + > /var/www/html/config/mimetypealiases.json && \ + chown www-data:www-data /var/www/html/config/mimetypealiases.json' @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-js >/dev/null @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-db --repair-filecache >/dev/null - @# Files-list icons are served by ElpxPreviewProvider via - @# core/preview?...&mimeFallback=true; when an .elpx package has no - @# screenshot.png, the provider returns img/elpx-preview-fallback.png. - @# No additional mimetypealiases.json hack is needed for that. + @# `img/filetypes/exelearning.svg` and `img/filetypes/exelearning-legacy.svg` + @# are picked up automatically by `update-js` once the aliases above + @# point at those filenames. ElpxPreviewProvider still runs for files + @# that actually have a `screenshot.png` inside; for those, the + @# preview overrides the static MIME icon as in upstream Nextcloud. @"$(MAKE)" --no-print-directory seed-fixtures @echo @echo "================================================================" diff --git a/img/filetypes/exelearning-legacy.svg b/img/filetypes/exelearning-legacy.svg new file mode 100644 index 0000000..c8dcc18 --- /dev/null +++ b/img/filetypes/exelearning-legacy.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d0052dd..a4aa6ab 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,9 +14,37 @@ class Application extends App implements IBootstrap { public const APP_ID = 'exelearning'; + /** Modern eXeLearning package MIME (registered for `.elpx`). */ + public const PRIMARY_MIME_TYPE = 'application/vnd.exelearning.elpx'; + /** Legacy eXeLearning project MIME (registered for `.elp`). */ + public const LEGACY_MIME_TYPE = 'application/x-exelearning-legacy'; + + /** + * MIMEs that unambiguously identify an eXeLearning archive on disk. + * Used by {@see \OCA\ExeLearning\Service\PermissionService::isElpxFile()} + * to decide whether to claim a file by MIME alone — extensions are + * still checked separately. + */ + public const VENDOR_MIME_TYPES = [ + self::PRIMARY_MIME_TYPE, + 'application/x-exelearning', + self::LEGACY_MIME_TYPE, + ]; + + /** + * Broader list of MIMEs an `.elp(x)` file may carry on disk before the + * admin has registered the custom mapping (Nextcloud falls back to + * `application/zip` or `application/octet-stream` in that case). Kept + * for routing decisions where we already know the request is for an + * eXeLearning resource — do **not** use this for "is this an + * eXeLearning file" checks; that path needs `VENDOR_MIME_TYPES` + * combined with an extension check, otherwise every plain ZIP in + * the user's Files would be misclassified. + */ public const ALLOWED_MIME_TYPES = [ - 'application/vnd.exelearning.elpx', + self::PRIMARY_MIME_TYPE, 'application/x-exelearning', + self::LEGACY_MIME_TYPE, 'application/zip', 'application/octet-stream', ]; diff --git a/lib/Preview/ElpxPreviewProvider.php b/lib/Preview/ElpxPreviewProvider.php index 0881a89..7fcc181 100644 --- a/lib/Preview/ElpxPreviewProvider.php +++ b/lib/Preview/ElpxPreviewProvider.php @@ -25,10 +25,15 @@ */ class ElpxPreviewProvider implements IProviderV2 { /** - * Regex that matches every MIME alias an .elpx file may carry on disk. - * The double backslash is needed because Nextcloud wraps this in `#…#`. + * Regex that matches every MIME alias an `.elp(x)` file may carry on + * disk. We intentionally cast a wide net here (`zip`, `octet-stream`) + * so Nextcloud asks us about every candidate file; the actual + * "is this really an eXeLearning archive" gate runs in + * {@see isAvailable()} via {@see PermissionService::isElpxFile()}, + * which now requires either a `.elp(x)` extension or a vendor- + * specific MIME (issue #21). */ - public const MIME_REGEX = '/^application\/(vnd\.exelearning\.elpx|x-exelearning|zip|octet-stream)$/'; + public const MIME_REGEX = '/^application\/(vnd\.exelearning\.elpx|x-exelearning(-legacy)?|zip|octet-stream)$/'; /** Bundled bitmap returned when the package has no screenshot.png. */ private const FALLBACK_IMAGE = __DIR__ . '/../../img/elpx-preview-fallback.png'; diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index eb167b1..b040480 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -32,8 +32,12 @@ public function isElpxFile(Node $node): bool { return true; } } + // Only accept MIME-based matches for genuinely vendor-specific + // types. `application/zip` and `application/octet-stream` would + // drag every plain ZIP / unknown-binary in the user's Files into + // our preview provider, which is the bug behind issue #21. $mime = strtolower((string)$node->getMimeType()); - return in_array($mime, Application::ALLOWED_MIME_TYPES, true); + return in_array($mime, Application::VENDOR_MIME_TYPES, true); } public function isReadable(Node $node): bool { diff --git a/tests/Unit/AppInfo/ApplicationConstantsTest.php b/tests/Unit/AppInfo/ApplicationConstantsTest.php index 5c4752b..8424fc8 100644 --- a/tests/Unit/AppInfo/ApplicationConstantsTest.php +++ b/tests/Unit/AppInfo/ApplicationConstantsTest.php @@ -22,4 +22,26 @@ public function testAllowedMimeTypesIncludesLegacyZipFallback(): void { self::assertContains('application/zip', Application::ALLOWED_MIME_TYPES); self::assertContains('application/octet-stream', Application::ALLOWED_MIME_TYPES); } + + public function testAllowedMimeTypesIncludesLegacyVendorMime(): void { + self::assertContains(Application::LEGACY_MIME_TYPE, Application::ALLOWED_MIME_TYPES); + self::assertSame('application/x-exelearning-legacy', Application::LEGACY_MIME_TYPE); + } + + /** + * VENDOR_MIME_TYPES is the narrow list used by + * {@see \OCA\ExeLearning\Service\PermissionService::isElpxFile()} to + * decide whether to claim a file by MIME alone. Issue #21 hinged on + * `application/zip` leaking into that path; pin it out here so a + * future edit cannot regress. + */ + public function testVendorMimeTypesDoesNotIncludeGenericArchiveMimes(): void { + self::assertNotContains('application/zip', Application::VENDOR_MIME_TYPES); + self::assertNotContains('application/octet-stream', Application::VENDOR_MIME_TYPES); + } + + public function testVendorMimeTypesCoversBothPackageGenerations(): void { + self::assertContains(Application::PRIMARY_MIME_TYPE, Application::VENDOR_MIME_TYPES); + self::assertContains(Application::LEGACY_MIME_TYPE, Application::VENDOR_MIME_TYPES); + } } From c18dc6e695759cb74ee2e01ad27b2a36ac5404cb Mon Sep 17 00:00:00 2001 From: erseco Date: Thu, 14 May 2026 23:09:44 +0100 Subject: [PATCH 2/4] fix(icons): copy SVGs into core/img/filetypes from `make up` + add "Open as eXeLearning" kebab on plain `.zip` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups after manual verification on the running stack: 1. Icon discovery: `maintenance:mimetype:update-js` only scans `core/img/filetypes/` (see the comment at the top of the generated `mimetypelist.js`). Apps' `img/filetypes/*.svg` are never picked up automatically, so even with the alias entries in `mimetypealiases.json` the modern and legacy icons fell back to the generic file glyph. `make up` now copies both `exelearning.svg` and `exelearning-legacy.svg` from the staged `custom_apps/exelearning/img/filetypes/` into `core/img/filetypes/` before regenerating the JS list. Production installs need the equivalent admin step — README updated with the new install snippet (and the legacy icon added to the existing alias example). 2. New kebab action `Open as eXeLearning` on plain `.zip` files (extension or `application/zip` mime), shown only when our default action is NOT already claiming the file. Lets users open a `.elpx` that has been re-saved with the wrong extension (or any zip they suspect is an eXeLearning project) without making the viewer the default for every archive in their account. Navigates to the existing `view?fileId=` route, which surfaces the legacy / invalid branches from #20 cleanly. Verified in browser: - `.zip` → gray archive icon, kebab includes "Open as eXeLearning". - `.elp` → distinct legacy icon (the green eXe + dark globe X). - `.elpx` → modern teal mark (unchanged). --- Makefile | 20 +++++++++++++++----- README.md | 34 ++++++++++++++++++++++------------ src/files/actions.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index b0bf1ac..c9c40af 100644 --- a/Makefile +++ b/Makefile @@ -417,13 +417,23 @@ up: check-docker 'echo "{\"application/vnd.exelearning.elpx\":\"exelearning\", \"application/x-exelearning\":\"exelearning\", \"application/x-exelearning-legacy\":\"exelearning-legacy\"}" \ > /var/www/html/config/mimetypealiases.json && \ chown www-data:www-data /var/www/html/config/mimetypealiases.json' + @# Nextcloud's `maintenance:mimetype:update-js` only scans + @# `core/img/filetypes/` for icon SVGs (see the comment at the top + @# of `core/js/mimetypelist.js`). App-shipped filetype icons are + @# not auto-discovered, so for the dev stack we copy ours into + @# core/ before regenerating the JS list. Production installs need + @# the admin to do the equivalent — see README → "Custom MIME icons". + @echo ">> copying app filetype icons into core/img/filetypes" + @docker exec $(DOCKER_NAME) bash -c \ + 'cp /var/www/html/custom_apps/exelearning/img/filetypes/exelearning.svg \ + /var/www/html/custom_apps/exelearning/img/filetypes/exelearning-legacy.svg \ + /var/www/html/core/img/filetypes/ && \ + chown www-data:www-data /var/www/html/core/img/filetypes/exelearning*.svg' @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-js >/dev/null @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-db --repair-filecache >/dev/null - @# `img/filetypes/exelearning.svg` and `img/filetypes/exelearning-legacy.svg` - @# are picked up automatically by `update-js` once the aliases above - @# point at those filenames. ElpxPreviewProvider still runs for files - @# that actually have a `screenshot.png` inside; for those, the - @# preview overrides the static MIME icon as in upstream Nextcloud. + @# ElpxPreviewProvider still runs for files that actually have a + @# `screenshot.png` inside; for those, the preview overrides the + @# static MIME icon as in upstream Nextcloud. @"$(MAKE)" --no-print-directory seed-fixtures @echo @echo "================================================================" diff --git a/README.md b/README.md index 89e4280..c3a8f4b 100644 --- a/README.md +++ b/README.md @@ -218,10 +218,15 @@ admin install should also configure mapping: ```json { - "elpx": ["application/vnd.exelearning.elpx", "application/zip"] + "elpx": ["application/vnd.exelearning.elpx", "application/zip"], + "elp": ["application/x-exelearning-legacy", "application/zip"] } ``` +The legacy MIME (`application/x-exelearning-legacy`) is what lets the +Files app render a different icon for pre-`.elpx` projects so users can +spot at a glance which packages still need migrating. + Then refresh Nextcloud's MIME caches: ```bash @@ -231,31 +236,33 @@ sudo -E -u www-data php occ maintenance:mimetype:update-db --repair-filecache Do **not** edit Nextcloud core `mimetypemapping.dist.json` directly. -### Optional: a static `.elpx` MIME icon +### Static `.elp(x)` MIME icons (recommended) The Files list normally shows the preview provided by `ElpxPreviewProvider` (the package's `screenshot.png`, or the bundled -fallback when there is none), so most installs do not need anything -else. If you want `.elpx` files to display a custom icon **outside** -of the preview path — sharing dialogs, breadcrumbs, contexts that -bypass `core/preview` — the documented (admin-side) procedure is: +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 icons too: -1. Add an alias to `config/mimetypealiases.json`: +1. Add the aliases to `config/mimetypealiases.json`: ```json { "application/vnd.exelearning.elpx": "exelearning", - "application/x-exelearning": "exelearning" + "application/x-exelearning": "exelearning", + "application/x-exelearning-legacy": "exelearning-legacy" } ``` -2. Copy this app's icon into Nextcloud core (the only directory - Nextcloud's `MimeIconProvider` scans): +2. Copy both icons into Nextcloud core (the only directory + `maintenance:mimetype:update-js` scans for SVGs — see the comment + at the top of `core/js/mimetypelist.js`): ```bash 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 + /var/www/nextcloud/apps/exelearning/img/filetypes/exelearning-legacy.svg \ + /var/www/nextcloud/core/img/filetypes/ ``` 3. Refresh the MIME caches again: @@ -266,7 +273,10 @@ bypass `core/preview` — the documented (admin-side) procedure is: ``` Step 2 is brittle because Nextcloud upgrades may replace `core/img/`; -restore it after each upgrade or stage it via a theme override. +restore both SVGs after each upgrade or stage them via a theme +override. The dev stack (`make up`) does this automatically — see the +`registering .elp(x) MIME mapping + icon aliases` step in the +Makefile. ## Viewer integration diff --git a/src/files/actions.ts b/src/files/actions.ts index 22330db..2db2a7b 100644 --- a/src/files/actions.ts +++ b/src/files/actions.ts @@ -103,6 +103,36 @@ const editAction: IFileAction = { }, } +// Lets users open a non-.elp(x) zip with the eXeLearning viewer (e.g. +// after `unzip` produced a folder they want to repack and previewed, +// or when a `.elpx` was renamed to `.zip`). Hidden for files we already +// claim by default and for things that aren't zips at all so the menu +// doesn't get crowded. +const openAsExeLearningAction: IFileAction = { + id: 'exelearning-open-as', + displayName: () => t(APP_ID, 'Open as eXeLearning'), + iconSvgInline: () => eyeIconSvgInline(), + enabled: ({ nodes }) => { + if (nodes.length !== 1) return false + const node = nodes[0] as Node + const shape = node as unknown as NodeShape + // Skip if our default action already handles it. + if (isElpxNode(node)) return false + const name = (shape.basename ?? shape.displayName ?? '').toLowerCase() + const mime = (shape.mime ?? '').toLowerCase() + return name.endsWith('.zip') || mime === 'application/zip' + }, + async exec({ nodes }) { + const file = nodes[0] + const fileId = (file as unknown as NodeShape).fileid ?? file.fileid + const url = generateUrl('/apps/exelearning/view?fileId={fileId}', { + fileId: String(fileId ?? ''), + }) + window.open(url, '_self') + return null + }, +} + const downloadAction: IFileAction = { id: 'exelearning-download', displayName: () => t(APP_ID, 'Download .elpx'), @@ -131,4 +161,5 @@ export function registerFileActions(): void { registerFileAction(viewAction) // default — opens /apps/exelearning/view registerFileAction(editAction) // kebab — opens /apps/exelearning/editor registerFileAction(downloadAction) // kebab — native download + registerFileAction(openAsExeLearningAction) // kebab on plain .zip } From 1851d3762a08450ad117b2ea8aea35f4d0b1f3d5 Mon Sep 17 00:00:00 2001 From: erseco Date: Thu, 14 May 2026 23:36:05 +0100 Subject: [PATCH 3/4] =?UTF-8?q?simplify(icons):=20drop=20the=20legacy-MIME?= =?UTF-8?q?=20branch=20=E2=80=94=20`.elp`=20and=20`.elpx`=20share=20one=20?= =?UTF-8?q?icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual testing surfaced two facts: 1. The `.elp` icon distinguished cleanly in list-view but not in grid-view, because grid-view uses the preview provider's fallback PNG (the modern brand mark) for both file types. A full fix would have meant shipping a second fallback PNG, branching the provider, and managing two assets in sync — extra surface for a feature that is more "nice to have" than load-bearing now that issue #20 already gives the user a clear in-app migration path. 2. The user just wants `.elp` to look like `.elpx` (and `.zip` to stop looking like an eXeLearning file). So strip the legacy-MIME branch: - `Application::LEGACY_MIME_TYPE` removed; `VENDOR_MIME_TYPES` is now just the modern + the `application/x-exelearning` alias. - `ElpxPreviewProvider::MIME_REGEX` no longer matches the `-legacy` variant, and `getThumbnail` no longer branches on extension. - `make up` writes `.elp → application/vnd.exelearning.elpx` (same as `.elpx`) and a single-icon alias file. Only `exelearning.svg` is copied into `core/img/filetypes/`. - `img/filetypes/exelearning-legacy.svg` deleted. - README and constants test updated to match. The two parts of issue #21 that *did* land stay in place: - The `isElpxFile()` tightening that stops `.zip` from inheriting the eXeLearning preview (the original bug). - The "Open as eXeLearning" kebab action on plain `.zip` files. Verified in browser (grid + list): `.elp` and `.elpx` show the modern teal mark, plain `.zip` shows the default archive icon. 51/51 vitest, 9/9 PHPUnit, typecheck/lint/build/cs-check green. --- Makefile | 29 +++++++++--------- README.md | 30 +++++++++---------- img/filetypes/exelearning-legacy.svg | 15 ---------- lib/AppInfo/Application.php | 6 +--- lib/Preview/ElpxPreviewProvider.php | 2 +- .../Unit/AppInfo/ApplicationConstantsTest.php | 8 +---- 6 files changed, 31 insertions(+), 59 deletions(-) delete mode 100644 img/filetypes/exelearning-legacy.svg diff --git a/Makefile b/Makefile index c9c40af..2e85c8b 100644 --- a/Makefile +++ b/Makefile @@ -399,22 +399,22 @@ up: check-docker @docker exec -u www-data $(DOCKER_NAME) php occ config:system:set apps_paths 1 writable --value=true --type=boolean >/dev/null @echo ">> enabling exelearning" @docker exec -u www-data $(DOCKER_NAME) php occ app:enable exelearning - @# `.elp(x)` → MIME mapping + icon aliases. These are the only - @# pieces a Nextcloud app cannot ship by itself: both files are - @# read from /var/www/html/config/. See README → "Custom MIME types" - @# for the manual admin steps required on production installs. + @# `.elp(x)` → MIME mapping + icon alias. These are the only pieces + @# a Nextcloud app cannot ship by itself: both files are read from + @# /var/www/html/config/. See README → "Custom MIME types" for the + @# manual admin steps required on production installs. @# - @# `.elpx` files get the modern vendor MIME; `.elp` files get the - @# legacy MIME so the Files app can render the older logo for them - @# (issue #21). `application/zip` stays out of the alias file on - @# purpose — aliasing it would tag every plain ZIP with our icon. - @echo ">> registering .elp(x) MIME mapping + icon aliases" + @# Both `.elpx` and `.elp` get the same vendor MIME so they share + @# the same eXeLearning icon. `application/zip` stays out of the + @# alias file on purpose — aliasing it would tag every plain ZIP + @# with our icon (the original symptom of issue #21). + @echo ">> registering .elp(x) MIME mapping + icon alias" @docker exec $(DOCKER_NAME) bash -c \ - 'echo "{\"elpx\":[\"application/vnd.exelearning.elpx\",\"application/zip\"], \"elp\":[\"application/x-exelearning-legacy\",\"application/zip\"]}" \ + 'echo "{\"elpx\":[\"application/vnd.exelearning.elpx\",\"application/zip\"], \"elp\":[\"application/vnd.exelearning.elpx\",\"application/zip\"]}" \ > /var/www/html/config/mimetypemapping.json && \ chown www-data:www-data /var/www/html/config/mimetypemapping.json' @docker exec $(DOCKER_NAME) bash -c \ - 'echo "{\"application/vnd.exelearning.elpx\":\"exelearning\", \"application/x-exelearning\":\"exelearning\", \"application/x-exelearning-legacy\":\"exelearning-legacy\"}" \ + 'echo "{\"application/vnd.exelearning.elpx\":\"exelearning\", \"application/x-exelearning\":\"exelearning\"}" \ > /var/www/html/config/mimetypealiases.json && \ chown www-data:www-data /var/www/html/config/mimetypealiases.json' @# Nextcloud's `maintenance:mimetype:update-js` only scans @@ -423,12 +423,11 @@ up: check-docker @# not auto-discovered, so for the dev stack we copy ours into @# core/ before regenerating the JS list. Production installs need @# the admin to do the equivalent — see README → "Custom MIME icons". - @echo ">> copying app filetype icons into core/img/filetypes" + @echo ">> copying eXeLearning filetype icon into core/img/filetypes" @docker exec $(DOCKER_NAME) bash -c \ 'cp /var/www/html/custom_apps/exelearning/img/filetypes/exelearning.svg \ - /var/www/html/custom_apps/exelearning/img/filetypes/exelearning-legacy.svg \ - /var/www/html/core/img/filetypes/ && \ - chown www-data:www-data /var/www/html/core/img/filetypes/exelearning*.svg' + /var/www/html/core/img/filetypes/exelearning.svg && \ + chown www-data:www-data /var/www/html/core/img/filetypes/exelearning.svg' @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-js >/dev/null @docker exec -u www-data $(DOCKER_NAME) php occ maintenance:mimetype:update-db --repair-filecache >/dev/null @# ElpxPreviewProvider still runs for files that actually have a diff --git a/README.md b/README.md index c3a8f4b..a0b93e6 100644 --- a/README.md +++ b/README.md @@ -219,13 +219,14 @@ admin install should also configure mapping: ```json { "elpx": ["application/vnd.exelearning.elpx", "application/zip"], - "elp": ["application/x-exelearning-legacy", "application/zip"] + "elp": ["application/vnd.exelearning.elpx", "application/zip"] } ``` -The legacy MIME (`application/x-exelearning-legacy`) is what lets the -Files app render a different icon for pre-`.elpx` projects so users can -spot at a glance which packages still need migrating. +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: @@ -236,33 +237,31 @@ sudo -E -u www-data php occ maintenance:mimetype:update-db --repair-filecache Do **not** edit Nextcloud core `mimetypemapping.dist.json` directly. -### Static `.elp(x)` MIME icons (recommended) +### Static `.elp(x)` MIME icon (recommended) 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 icons too: +configure the static MIME icon too: -1. Add the aliases to `config/mimetypealiases.json`: +1. Add the alias to `config/mimetypealiases.json`: ```json { "application/vnd.exelearning.elpx": "exelearning", - "application/x-exelearning": "exelearning", - "application/x-exelearning-legacy": "exelearning-legacy" + "application/x-exelearning": "exelearning" } ``` -2. Copy both icons into Nextcloud core (the only directory +2. Copy this app's icon into Nextcloud core (the only directory `maintenance:mimetype:update-js` scans for SVGs — see the comment at the top of `core/js/mimetypelist.js`): ```bash sudo install -o www-data -g www-data -m 0644 \ /var/www/nextcloud/apps/exelearning/img/filetypes/exelearning.svg \ - /var/www/nextcloud/apps/exelearning/img/filetypes/exelearning-legacy.svg \ - /var/www/nextcloud/core/img/filetypes/ + /var/www/nextcloud/core/img/filetypes/exelearning.svg ``` 3. Refresh the MIME caches again: @@ -273,10 +272,9 @@ configure the static MIME icons too: ``` Step 2 is brittle because Nextcloud upgrades may replace `core/img/`; -restore both SVGs after each upgrade or stage them via a theme -override. The dev stack (`make up`) does this automatically — see the -`registering .elp(x) MIME mapping + icon aliases` step in the -Makefile. +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. ## Viewer integration diff --git a/img/filetypes/exelearning-legacy.svg b/img/filetypes/exelearning-legacy.svg deleted file mode 100644 index c8dcc18..0000000 --- a/img/filetypes/exelearning-legacy.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a4aa6ab..d7a6019 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,10 +14,8 @@ class Application extends App implements IBootstrap { public const APP_ID = 'exelearning'; - /** Modern eXeLearning package MIME (registered for `.elpx`). */ + /** Modern eXeLearning package MIME, registered for both `.elp` and `.elpx`. */ public const PRIMARY_MIME_TYPE = 'application/vnd.exelearning.elpx'; - /** Legacy eXeLearning project MIME (registered for `.elp`). */ - public const LEGACY_MIME_TYPE = 'application/x-exelearning-legacy'; /** * MIMEs that unambiguously identify an eXeLearning archive on disk. @@ -28,7 +26,6 @@ class Application extends App implements IBootstrap { public const VENDOR_MIME_TYPES = [ self::PRIMARY_MIME_TYPE, 'application/x-exelearning', - self::LEGACY_MIME_TYPE, ]; /** @@ -44,7 +41,6 @@ class Application extends App implements IBootstrap { public const ALLOWED_MIME_TYPES = [ self::PRIMARY_MIME_TYPE, 'application/x-exelearning', - self::LEGACY_MIME_TYPE, 'application/zip', 'application/octet-stream', ]; diff --git a/lib/Preview/ElpxPreviewProvider.php b/lib/Preview/ElpxPreviewProvider.php index 7fcc181..5868eda 100644 --- a/lib/Preview/ElpxPreviewProvider.php +++ b/lib/Preview/ElpxPreviewProvider.php @@ -33,7 +33,7 @@ class ElpxPreviewProvider implements IProviderV2 { * which now requires either a `.elp(x)` extension or a vendor- * specific MIME (issue #21). */ - public const MIME_REGEX = '/^application\/(vnd\.exelearning\.elpx|x-exelearning(-legacy)?|zip|octet-stream)$/'; + public const MIME_REGEX = '/^application\/(vnd\.exelearning\.elpx|x-exelearning|zip|octet-stream)$/'; /** Bundled bitmap returned when the package has no screenshot.png. */ private const FALLBACK_IMAGE = __DIR__ . '/../../img/elpx-preview-fallback.png'; diff --git a/tests/Unit/AppInfo/ApplicationConstantsTest.php b/tests/Unit/AppInfo/ApplicationConstantsTest.php index 8424fc8..a1e6e83 100644 --- a/tests/Unit/AppInfo/ApplicationConstantsTest.php +++ b/tests/Unit/AppInfo/ApplicationConstantsTest.php @@ -23,11 +23,6 @@ public function testAllowedMimeTypesIncludesLegacyZipFallback(): void { self::assertContains('application/octet-stream', Application::ALLOWED_MIME_TYPES); } - public function testAllowedMimeTypesIncludesLegacyVendorMime(): void { - self::assertContains(Application::LEGACY_MIME_TYPE, Application::ALLOWED_MIME_TYPES); - self::assertSame('application/x-exelearning-legacy', Application::LEGACY_MIME_TYPE); - } - /** * VENDOR_MIME_TYPES is the narrow list used by * {@see \OCA\ExeLearning\Service\PermissionService::isElpxFile()} to @@ -40,8 +35,7 @@ public function testVendorMimeTypesDoesNotIncludeGenericArchiveMimes(): void { self::assertNotContains('application/octet-stream', Application::VENDOR_MIME_TYPES); } - public function testVendorMimeTypesCoversBothPackageGenerations(): void { + public function testVendorMimeTypesIncludesPrimaryMime(): void { self::assertContains(Application::PRIMARY_MIME_TYPE, Application::VENDOR_MIME_TYPES); - self::assertContains(Application::LEGACY_MIME_TYPE, Application::VENDOR_MIME_TYPES); } } From 40fb2886167e63dec1e307b31c975f136e4ca009 Mon Sep 17 00:00:00 2001 From: erseco Date: Thu, 14 May 2026 23:44:27 +0100 Subject: [PATCH 4/4] fix(view): drop the "is .elp(x)" gate from `assertAllowed` so "Open as eXeLearning" works on `.zip` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, opening a plain `.zip` via the new kebab action 404'd in the controller (`Not an eXeLearning package`), which the page renders as "No file selected. Back to Files". The MIME / extension check stays in `PermissionService::isElpxFile()` because the preview provider still relies on it (we do NOT want every ZIP in Files to inherit our preview thumbnail). The viewer / editor flow, on the other hand, is reached only when the user explicitly clicks an action — at that point bouncing them with a generic 404 is worse than letting the downstream validator surface a clear "not an eXeLearning project" message. Auth + ownership are still enforced by Nextcloud's `getById()` resolving against the user's home folder, and the size limit gate stays in place. So the only thing that changes is: the controller hands a permitted-but-non-vendor file to the frontend, which then runs `validatePackage()` from issue #20 and shows the legacy / invalid branches as appropriate. --- lib/Service/ElpxPackageService.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/Service/ElpxPackageService.php b/lib/Service/ElpxPackageService.php index 9cd1ab3..b13f743 100644 --- a/lib/Service/ElpxPackageService.php +++ b/lib/Service/ElpxPackageService.php @@ -61,9 +61,15 @@ private function assertAllowed(Node $node): void { if (!$this->permissions->isReadable($node)) { throw new NotPermittedException('No read permission'); } - if (!$this->permissions->isElpxFile($node)) { - throw new NotPermittedException('Not an eXeLearning package'); - } + // We intentionally do NOT gate on `isElpxFile()` here. That check + // is still used by `ElpxPreviewProvider` (so plain ZIPs do not + // inherit our preview), but the viewer / editor flow should + // also accept files the user explicitly opened via the + // "Open as eXeLearning" kebab on a plain `.zip`. The downstream + // validator (`src/elpx/package-validator.ts`) still surfaces a + // clean error for archives that are not actually eXeLearning + // projects, and the user's auth + ownership are already + // guaranteed by `getById()` resolving to a node they can read. if (!$this->permissions->isWithinSizeLimit($node)) { throw new NotPermittedException('Package too large'); }