Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

feat(resources): public resource serve + listing endpoints#53

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feature/impl-resource-serving
Apr 30, 2026
Merged

feat(resources): public resource serve + listing endpoints#53
rubenvdlinde merged 1 commit into
developmentfrom
feature/impl-resource-serving

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

Adds the read side of the resource-uploads capability per REQ-RES-006..008, complementing the admin-only upload pipeline merged in #46.

  • GET /apps/mydash/resource/{filename} — non-OCS plain web route streaming bytes via php://memory with extension-derived Content-Type and Cache-Control: public, max-age=31536000.
  • GET /apps/mydash/api/resources — listing endpoint returning {status, resources: [{name, url, size, modifiedAt}]} ordered by modifiedAt desc; empty folder returns HTTP 200 with [].
  • Path traversal blocked at routing layer ([^/]+) plus controller-level defence in depth (.., \\, empty/dot values) yielding uniform HTTP 404.
  • 50 MB+ files (only reachable via manual filesystem tampering) refused with HTTP 413 BEFORE bytes are loaded — bounds memory to the 5 MB upload cap from REQ-RES-003.
  • uniqid suffix on REQ-RES-004 filenames doubles as cache buster — a logical asset change yields a brand-new filename, naturally bypassing the one-year immutable cache.

Implementation notes

  • New ResourceServeController + ResourceServeService pair, kept separate from the upload ResourceController so the dependency graph stays under the PHPMD CouplingBetweenObjects limit.
  • Both endpoints carry @NoAdminRequired — admin gating would lock dashboards out of their own assets, since uploaded resources are referenced from every dashboard render.
  • Routes registered under the standard routes array (mydash currently has no OCS controller infrastructure). Pragmatic deviation from tasks.md 2.2 (which suggested registering listResources under ocs) — keeps the read surface consistent with the existing POST /api/resources upload route on the same path. Behaviourally identical from the dashboard's perspective.
  • OpenAPI spec update (task 5.2) skipped — no OpenAPI artefact exists in this repo to update.
  • Pre-existing PHPStan warning in ImageMimeValidator fixed in passing (dead ?? '' on a non-nullable field).

Test plan

  • PHPUnit: 19 new tests covering all 8 spec scenarios — PNG bytes + headers, SVG image/svg+xml content-type, unknown ext → octet-stream, missing file → 404, encoded traversal → 404 (8 vectors via @dataProvider), oversize → 413 with no getContent() call, list ordering desc, empty folder → []. All 208 tests pass.
  • composer check:strict — lint, phpcs, phpmd, psalm, phpstan, test:all all green.
  • SPDX headers on every new PHP file (in the docblock per the SPDX-in-docblock convention).
  • Manual smoke test in the dev container (uploading via the existing admin UI then fetching the served bytes) — to be validated by the reviewer during code review.
  • Playwright tasks 4.1/4.2 (image widget renders the served URL; unauthenticated request redirects to login) — left for a follow-up Playwright change since the existing repo has no Playwright suite yet.

@rubenvdlinde rubenvdlinde added the ready-for-code-review Build complete — awaiting code reviewer label Apr 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 8c7c677

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:34 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde force-pushed the feature/impl-resource-serving branch 2 times, most recently from ccb8c0d to b30cbc7 Compare April 30, 2026 12:36
Add the read side of the resource-uploads capability that complements
the admin-only upload pipeline merged in PR #46:

- Non-OCS GET /apps/mydash/resource/{filename} returning a
  StreamResponse with extension-derived Content-Type (jpeg/png/gif/
  svg+xml/webp; default application/octet-stream) and
  Cache-Control: public, max-age=31536000.
- GET /apps/mydash/api/resources listing endpoint returning
  {status, resources: [{name, url, size, modifiedAt}]} ordered by
  modifiedAt desc; empty folder yields HTTP 200 with [] (never 404).
- Path traversal blocked at routing layer ([^/]+) plus defence-in-depth
  controller checks for .., backslashes, and empty values. All map to
  a uniform HTTP 404 with no detail leaked.
- 50MB+ files (only reachable via manual filesystem tampering) refused
  with HTTP 413 BEFORE bytes are loaded — bounding worst-case memory
  to the 5 MB upload cap from REQ-RES-003.
- Cache-busting via uniqid suffix in REQ-RES-004 filenames — a logical
  asset change yields a brand-new filename, naturally bypassing the
  one-year immutable cache.

Implementation lives in a dedicated ResourceServeController +
ResourceServeService pair so the existing upload controller's
dependency graph stays under the PHPMD CouplingBetweenObjects limit.

Routes registered under the standard `routes` array (mydash currently
has no OCS controller infrastructure); both endpoints carry
@NoAdminRequired so any logged-in user can fetch dashboard assets.

Also fixes a pre-existing PHPStan warning in ImageMimeValidator
(getimagesizefromstring always returns mime, ?? '' was dead).

Tests: 19 new unit tests (8 ResourceServeController, 11
ResourceServeService) covering all 8 spec scenarios — PNG bytes +
headers, SVG content-type, unknown ext fallback, missing file 404,
encoded path traversal 404 (8 vectors via dataProvider), oversize
413 with no read, list ordering desc, empty folder empty array. All
208 PHPUnit tests pass.

Quality: composer check:strict passes (lint, phpcs, phpmd, psalm,
phpstan, test:all). SPDX headers on all new PHP files.
@rubenvdlinde rubenvdlinde force-pushed the feature/impl-resource-serving branch from b30cbc7 to b769a6a Compare April 30, 2026 12:36
@rubenvdlinde rubenvdlinde merged commit 4f42fbb into development Apr 30, 2026
30 of 32 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/impl-resource-serving branch April 30, 2026 12:36
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 0352b88

Check PHP Vue Security License Tests
lint ⏭️
phpcs ⏭️
phpmd ⏭️
psalm ⏭️
phpstan ⏭️
phpmetrics
eslint
stylelint ⏭️
composer ⏭️ ⏭️
npm ⏭️ ⏭️
PHPUnit
Newman
Playwright

Quality workflow — 2026-04-30 12:36 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ cf90700

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:38 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request May 3, 2026
…53)

Add the read side of the resource-uploads capability that complements
the admin-only upload pipeline merged in PR #46:

- Non-OCS GET /apps/mydash/resource/{filename} returning a
  StreamResponse with extension-derived Content-Type (jpeg/png/gif/
  svg+xml/webp; default application/octet-stream) and
  Cache-Control: public, max-age=31536000.
- GET /apps/mydash/api/resources listing endpoint returning
  {status, resources: [{name, url, size, modifiedAt}]} ordered by
  modifiedAt desc; empty folder yields HTTP 200 with [] (never 404).
- Path traversal blocked at routing layer ([^/]+) plus defence-in-depth
  controller checks for .., backslashes, and empty values. All map to
  a uniform HTTP 404 with no detail leaked.
- 50MB+ files (only reachable via manual filesystem tampering) refused
  with HTTP 413 BEFORE bytes are loaded — bounding worst-case memory
  to the 5 MB upload cap from REQ-RES-003.
- Cache-busting via uniqid suffix in REQ-RES-004 filenames — a logical
  asset change yields a brand-new filename, naturally bypassing the
  one-year immutable cache.

Implementation lives in a dedicated ResourceServeController +
ResourceServeService pair so the existing upload controller's
dependency graph stays under the PHPMD CouplingBetweenObjects limit.

Routes registered under the standard `routes` array (mydash currently
has no OCS controller infrastructure); both endpoints carry
@NoAdminRequired so any logged-in user can fetch dashboard assets.

Also fixes a pre-existing PHPStan warning in ImageMimeValidator
(getimagesizefromstring always returns mime, ?? '' was dead).

Tests: 19 new unit tests (8 ResourceServeController, 11
ResourceServeService) covering all 8 spec scenarios — PNG bytes +
headers, SVG content-type, unknown ext fallback, missing file 404,
encoded path traversal 404 (8 vectors via dataProvider), oversize
413 with no read, list ordering desc, empty folder empty array. All
208 PHPUnit tests pass.

Quality: composer check:strict passes (lint, phpcs, phpmd, psalm,
phpstan, test:all). SPDX headers on all new PHP files.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-for-code-review Build complete — awaiting code reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant