Skip to content

Add optional asset proxy mode for servers with incorrect MIME types#54

Open
erseco wants to merge 1 commit into
mainfrom
feature/53-asset-proxy-mode
Open

Add optional asset proxy mode for servers with incorrect MIME types#54
erseco wants to merge 1 commit into
mainfrom
feature/53-asset-proxy-mode

Conversation

@erseco

@erseco erseco commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Closes #53

The problem

Some WordPress hosts serve files from the uploads directory with an incomplete or incorrect MIME map. When a package's JavaScript is returned as text/plain while X-Content-Type-Options: nosniff is enabled, the browser refuses to execute the script, even though the file exists and the HTTP status is 200:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
X-Content-Type-Options: nosniff
Refused to execute script because its MIME type ('text/plain') is not
executable, and strict MIME type checking is enabled.

The package HTML itself loads fine (it already goes through the REST content proxy), but the directly-served sub-assets (CSS, JS, fonts, images, media) are rejected and the package breaks.

Root cause

The plugin routes script-capable documents (html, htm, svg, xml) through the content proxy for hardened headers, but serves sub-assets directly from wp-content/uploads/exelearning/{hash}/, trusting the web server to set the correct MIME type. On a misconfigured server that trust fails.

The fix

A new optional, default-off setting routes all package assets through the existing content proxy, which sends explicit Content-Type headers (application/javascript, text/css, valid font/image types…). Direct uploads URLs remain the default for performance.

The change is intentionally small: the proxy already serves every extension with the right Content-Type and already rewrites HTML/CSS URLs with full path-traversal/realpath protection. The only behavioral change is widening ExeLearning_Content_Proxy::is_proxied_path() so that, when the setting is on, ordinary assets are routed through the proxy too. External URLs (https://, //, data:, blob:, #, javascript:) are still never proxied, and CSS url(...) references keep resolving relative to the CSS file.

What's included

  • Content proxy — new exelearning_proxy_assets option, a filterable is_asset_proxy_enabled() getter, widened is_proxied_path(), and .mjs mapped to application/javascript.
  • Settings — a new Content delivery section (Settings → eXeLearning) with a single checkbox "Serve package assets through the WordPress proxy", saved via a nonce-protected admin-ajax endpoint. Disabled by default.
  • Developer hookexelearning_proxy_assets filter to force the mode on/off per environment (documented in docs/HOOKS.md).
  • Translations — the new strings are translated for all 10 shipped locales (ca, ca_valencia, de_DE, eo, es_ES, eu, gl_ES, it_IT, pt_PT, ro_RO); .po/.mo/.pot regenerated.
  • Tests — proxy routing tests in ContentProxyTest and toggle/permission tests in AdminSettingsTest.

Acceptance criteria

  • New translatable setting to enable proxying package assets.
  • Disabled by default.
  • When disabled, current direct-asset behavior is preserved.
  • When enabled, package JavaScript is served as application/javascript.
  • CSS served as text/css; fonts/images with valid MIME types.
  • CSS url(...) references resolved relative to the CSS file location.
  • Path traversal / access outside the extracted directory remain blocked.
  • Help text warns the mode may be slower (assets go through PHP/WordPress).

How to test

Automated

make lint
make test FILTER=ContentProxy   # proxy routing logic
make test FILTER=AdminSettings  # settings toggle + permissions
make check-untranslated         # every locale fully translated

Manual (reproduce the off → on behavior)

  1. make up, log in at http://localhost:8888/wp-admin (admin / password).
  2. Upload an .elpx package to the Media Library and embed it on a page: [exelearning id="N"].
  3. Open the page with DevTools → Network, filtered to JS:
    • Setting OFF (default): the package's scripts load from …/wp-content/uploads/exelearning/{hash}/…js (served directly by the web server).
    • Go to Settings → eXeLearning → Content delivery and tick "Serve package assets through the WordPress proxy".
    • Setting ON: reload — the same scripts now load from …/wp-json/exelearning/v1/content/{hash}/…js with Content-Type: application/javascript, and CSS as text/css. The package runs even under strict nosniff MIME checking.

To simulate the broken host, configure the web server to return text/plain for .js under the uploads path: with the setting off the package fails with the “Refused to execute script” error; with it on the proxy sends the correct Content-Type and the package works.

Some servers return package JavaScript as text/plain while
X-Content-Type-Options: nosniff is set, so browsers refuse to execute it
and packages break. Add an optional, default-off "Content delivery"
setting that routes all package assets through the existing content
proxy, which sends explicit Content-Type headers. Direct uploads URLs
remain the default for performance.

- Content proxy: new exelearning_proxy_assets option and filter; widen
  is_proxied_path() to cover every package asset when enabled; map .mjs
  to application/javascript.
- Settings: new "Content delivery" section with an AJAX-saved checkbox.
- Docs: document the exelearning_proxy_assets filter.
- Translations updated for all 10 shipped locales.
- Tests for the proxy routing logic and the settings toggle.

Closes #53
@github-actions

Copy link
Copy Markdown
Contributor

Test in WordPress Playground

Test the plugin with the code from this branch:

Preview in WordPress Playground

ℹ️ The eXeLearning editor is fetched from the shared release and unpacked into the plugin when the playground boots, so the first load may take a few extra seconds. ELP upload, shortcode, Gutenberg block and preview work normally.

@erseco erseco self-assigned this Jun 10, 2026
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.

Add optional asset proxy mode for servers with incorrect MIME types

1 participant