Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cf389a2
feat(security): add secure (opaque-origin) content iframe mode
erseco Jun 13, 2026
9eb07ff
i18n: translate the secure-iframe admin strings; trim the settings copy
erseco Jun 13, 2026
1452468
style(admin): inline the mode write to satisfy phpcs alignment sniff
erseco Jun 13, 2026
7adca0c
fix(security): sandbox the content response in secure mode (raw-route…
erseco Jun 13, 2026
4416ccd
Render external embeds (YouTube/Vimeo/PDF) inline in secure mode
erseco Jun 14, 2026
4266410
Report absolute embed URLs from the shim (resolve relative srcs)
erseco Jun 14, 2026
76f40fe
Add Firefox e2e for secure-mode external embeds
erseco Jun 14, 2026
15f1b71
Keep the embed Firefox e2e out of the WordPress (chromium/wp-env) Pla…
erseco Jun 14, 2026
8fc1354
Mirror canonical embed updates: providers, clamp, allow-forms, page-n…
erseco Jun 14, 2026
53fd157
Fix PHPCS: capitalize the allow-forms docblock long description
erseco Jun 14, 2026
f162e72
Mirror canonical embeds: structural invariant (any cross-origin https…
erseco Jun 14, 2026
d04ed58
Update embed e2e for open mode: every cross-origin/PDF iframe promote…
erseco Jun 14, 2026
a9d39a3
Revert embed-policy admin UI (untranslated-strings i18n gate); keep o…
erseco Jun 14, 2026
b41dc7b
Mirror canonical embed hardening: trailing-dot gate, dead-code, Vitest
erseco Jun 17, 2026
f0a193b
Merge remote-tracking branch 'origin/main' into feature/secure-iframe…
erseco Jun 18, 2026
1db0efc
Merge branch 'main' into feature/secure-iframe-sandbox
erseco Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public/style/workarea/*.css.map
# Test results
test-results/
test-results/*
playwright-report/

# Built static editor - download from releases or build with `make build-editor`
dist/static/
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ Administrators can upload eXeLearning style packages and control which styles th

Uploaded ZIPs are validated against path traversal, absolute paths, oversize archives (default 20 MB, filterable via `exelearning_styles_max_zip_size`), and a strict file-extension allow-list.

## External embeds in secure mode

In secure mode the `.elpx` content runs in a sandboxed, opaque-origin iframe. That
opaque origin propagates to any nested iframe, so cross-origin video players and PDF
viewers render blank. To keep them working, whitelisted video embeds (YouTube and
Vimeo hosts), any cross-origin `https` `.pdf`, and the package's own local PDFs are
*promoted* to the trusted parent page and rendered inline on top of the content.

Two cooperating scripts make this work:

- `assets/js/exe-embed-shim.js` runs inside the content iframe, replaces each
promotable iframe with a same-size placeholder, and `postMessage`s its geometry
and URL to the parent.
- `assets/js/exe-embed-relay.js` runs on the host page, validates each reported URL
against the whitelist, rebuilds the canonical player URL, and overlays the real
player exactly over the placeholder.

A static Firefox end-to-end test exercises the real shim and relay against a
self-contained harness (no WordPress runtime needed):

```bash
npm run test:e2e:embed
```

## Developer hooks

The plugin exposes a set of WordPress actions and filters (all prefixed with
Expand Down
60 changes: 60 additions & 0 deletions admin/class-admin-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,73 @@ public function display_settings_page() {
<h1><?php esc_html_e( 'eXeLearning Settings', 'exelearning' ); ?></h1>

<?php $this->render_editor_status_section(); ?>
<?php $this->render_security_section(); ?>
<?php $this->render_styles_section(); ?>
<?php $this->render_content_delivery_section(); ?>
<?php $this->render_help_section(); ?>
</div>
<?php
}

/**
* Render the iframe security mode section and persist its form submission.
*
* Lets the admin choose how embedded eXeLearning content is sandboxed:
* - Secure (default): opaque-origin iframe; author HTML/JS cannot reach the
* WordPress page (recommended).
* - Legacy: same-origin iframe, for environments that need it (e.g. WordPress
* Playground, whose service worker only serves same-origin documents).
*/
private function render_security_section() {
if ( isset( $_POST['exelearning_iframe_mode_submit'] ) ) {
check_admin_referer( 'exelearning_iframe_mode' );
if ( current_user_can( 'manage_options' ) ) {
$submitted = isset( $_POST['exelearning_iframe_sandbox_mode'] )
? sanitize_key( wp_unslash( $_POST['exelearning_iframe_sandbox_mode'] ) )
: '';
update_option(
ExeLearning_Iframe_Sandbox::OPTION,
ExeLearning_Iframe_Sandbox::MODE_LEGACY === $submitted
? ExeLearning_Iframe_Sandbox::MODE_LEGACY
: ExeLearning_Iframe_Sandbox::MODE_SECURE
);
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Settings saved.', 'exelearning' ) . '</p></div>';
}
}

$current = ExeLearning_Iframe_Sandbox::mode();
?>
<div class="card" id="exelearning-security-card" style="max-width: 900px; margin-bottom: 20px;">
<h2><?php esc_html_e( 'Security', 'exelearning' ); ?></h2>
<form method="post">
<?php wp_nonce_field( 'exelearning_iframe_mode' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="exelearning_iframe_sandbox_mode"><?php esc_html_e( 'Iframe security mode', 'exelearning' ); ?></label>
</th>
<td>
<select name="exelearning_iframe_sandbox_mode" id="exelearning_iframe_sandbox_mode">
<option value="secure" <?php selected( $current, ExeLearning_Iframe_Sandbox::MODE_SECURE ); ?>>
<?php esc_html_e( 'Secure (opaque-origin sandbox)', 'exelearning' ); ?>
</option>
<option value="legacy" <?php selected( $current, ExeLearning_Iframe_Sandbox::MODE_LEGACY ); ?>>
<?php esc_html_e( 'Legacy (same-origin)', 'exelearning' ); ?>
</option>
</select>
<p class="description">
<?php esc_html_e( 'Secure (recommended) isolates embedded content in an opaque-origin iframe. Legacy keeps same-origin behavior, needed only in some environments such as WordPress Playground.', 'exelearning' ); ?>
</p>
</td>
</tr>
</table>
<?php submit_button( __( 'Save changes', 'exelearning' ), 'primary', 'exelearning_iframe_mode_submit' ); ?>
</form>
</div>
<?php
}

/**
* Render the help section.
*
Expand Down
Loading