Skip to content

feat(iframe-embed-widget): sandboxed external-URL embed + archive#14

Merged
rubenvdlinde merged 2 commits into
developmentfrom
wip/build-iframe-embed-2026-07-23
Jul 23, 2026
Merged

feat(iframe-embed-widget): sandboxed external-URL embed + archive#14
rubenvdlinde merged 2 commits into
developmentfrom
wip/build-iframe-embed-2026-07-23

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Implements and archives iframe-embed-widget. Embedding an external URL as a widget has been requested on Nextcloud since 2019 (dashboard#53) and is served today only by third-party micro-apps (iFrame Widget, External Portal, DashLink).

What

  • IframeWidget.vue / IframeWidgetForm.vue / iframeClient.js, registered as widget type iframe.
  • IframeServicefail-closed host allow-list (iframe_allowed_hosts; empty = deny everything) plus sandbox-token sanitisation.
  • CspListener — contributes allow-listed hosts to app-scoped frame-src via AddContentSecurityPolicyEvent. No wildcard.
  • POST /api/iframe/validate-url for save-time validation (registered before the catch-all route).

The X-Frame-Options reality

A target sending X-Frame-Options: DENY or frame-ancestors 'none' cannot be embedded, and no embedder can override that — so the widget detects the refusal instead of showing a blank frame. Two signals: no load event within a timeout, or a load whose contentDocument is same-origin-readable and empty. A SecurityError when reading contentDocument is treated as success — that's the normal signature of a legitimately-loaded cross-origin page. Both paths are unit-tested. Failure renders a fallback card (icon and text, never colour-only) with a keyboard-reachable "Open in new tab".

Verification (local, no CI wait)

  • npx vitest run (widget, form, registry) → 39 tests pass.
  • phpunit (IframeService, IframeController, CspListener) → OK, 27 tests, 36 assertions.
  • gate-16 PASS; no new gate failures (remaining reds all belong to pre-existing files).

Deviation, stated plainly

REQ-IFRAME-001 asks for lib/Widget/IframeWidgetProvider.php registering via OCP\\Dashboard\\IManager. This app has no PHP widget-provider layer at allclock, weather and livetile all register from src/constants/widgetRegistry.js. I followed the real pattern and left that scenario unchecked in tasks.md rather than reinterpret it silently. The spec text is the thing that's wrong here, not the code.

Also: the build agent had hand-written openspec/specs/iframe-embed-widget/spec.md; I removed it and let openspec archive generate it, so the canonical spec stays tool-owned.

Not done

Playwright e2e; an admin settings panel for iframe_allowed_hosts (config is occ-only, matching the livetile_allowed_hosts precedent).

…ail-closed allow-list + CSP frame-src

Adds a launchpad_iframe dashboard widget that embeds an admin-allow-listed
external URL in a sandboxed iframe. The allow-list (iframe_allowed_hosts
app config) is enforced fail-closed both at save time and at CSP-assembly
time, and is contributed host-by-host (never a wildcard) to LaunchPad's
own frame-src CSP directive via a new AddContentSecurityPolicyEvent
listener. The widget always carries a sandbox attribute (allow-top-
navigation can never be granted) and degrades gracefully to a fallback
card with an open-in-new-tab link when a target refuses framing via
X-Frame-Options/frame-ancestors, which cannot be overridden client-side.

Registered as type `iframe` in the frontend widgetRegistry.js catalog,
matching this app's actual architecture (no lib/Widget/ PHP-provider
layer exists for any widget type — clock/weather/livetile are the same
shape). REQ-IFRAME-001's IManager v2 registration scenario is left
unimplemented and documented as such in tasks.md; it does not match how
any widget in this app registers today.
Replaces the hand-written canonical spec with the one openspec archive
generates, so the spec is tool-owned rather than a manual mirror.
@rubenvdlinde
rubenvdlinde merged commit 2dc514f into development Jul 23, 2026
1 check failed
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.

1 participant