Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 101 additions & 0 deletions admin/class-admin-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
*/
class ExeLearning_Admin_Settings {

/**
* Nonce action for the content-delivery AJAX toggle.
*/
const PROXY_ASSETS_NONCE = 'exelearning_proxy_assets';

/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_filter( 'plugin_action_links_' . plugin_basename( EXELEARNING_PLUGIN_FILE ), array( $this, 'add_action_links' ) );
add_action( 'wp_ajax_exelearning_toggle_proxy_assets', array( $this, 'ajax_toggle_proxy_assets' ) );
}

/**
Expand Down Expand Up @@ -65,6 +71,7 @@ public function display_settings_page() {

<?php $this->render_editor_status_section(); ?>
<?php $this->render_styles_section(); ?>
<?php $this->render_content_delivery_section(); ?>
<?php $this->render_help_section(); ?>
</div>
<?php
Expand Down Expand Up @@ -158,6 +165,100 @@ private function render_help_section() {
<?php
}

/**
* Render the content-delivery section.
*
* Exposes the optional asset-proxy mode (issue #53): when enabled, package
* assets are served through the WordPress content proxy with explicit
* Content-Type headers instead of being linked directly from the uploads
* directory. The toggle is saved via an admin-ajax endpoint handled by
* {@see self::ajax_toggle_proxy_assets()}.
*/
private function render_content_delivery_section() {
$proxy_assets = (bool) get_option( ExeLearning_Content_Proxy::OPTION_PROXY_ASSETS, false );
$nonce = wp_create_nonce( self::PROXY_ASSETS_NONCE );
$ajax_url = admin_url( 'admin-ajax.php' );
?>
<div class="card" id="exelearning-content-delivery-card" style="max-width: 900px; margin-bottom: 20px;">
<h2><?php esc_html_e( 'Content delivery', 'exelearning' ); ?></h2>
<p>
<label>
<input type="checkbox" id="exelearning-proxy-assets" <?php checked( $proxy_assets ); ?> />
<strong><?php esc_html_e( 'Serve package assets through the WordPress proxy', 'exelearning' ); ?></strong>
</label>
</p>
<p class="description">
<?php esc_html_e( 'Use this option only if your web server returns incorrect MIME types for package assets, for example JavaScript files served as text/plain. When enabled, CSS, JavaScript, fonts, images and other package files are served through WordPress so the plugin can send explicit Content-Type headers. This can reduce performance because requests are handled by PHP instead of being served directly by the web server.', 'exelearning' ); ?>
</p>

<div id="exelearning-content-delivery-status" style="display: none; margin: 10px 0;"></div>
</div>

<script>
(function () {
var ajaxUrl = <?php echo wp_json_encode( $ajax_url ); ?>;
var nonce = <?php echo wp_json_encode( $nonce ); ?>;
var checkbox = document.getElementById('exelearning-proxy-assets');
var statusBox = document.getElementById('exelearning-content-delivery-status');

if (!checkbox) return;

function setError(message) {
if (!statusBox) return;
statusBox.style.display = 'block';
statusBox.innerHTML = '<div class="notice notice-error inline"><p></p></div>';
statusBox.querySelector('p').textContent = message;
}

checkbox.addEventListener('change', function () {
var fd = new FormData();
fd.append('action', 'exelearning_toggle_proxy_assets');
fd.append('_ajax_nonce', nonce);
fd.append('enabled', checkbox.checked ? '1' : '');
fetch(ajaxUrl, {
method: 'POST',
body: fd,
credentials: 'same-origin'
}).then(function (r) { return r.json(); }).then(function (resp) {
if (!resp || !resp.success) {
checkbox.checked = !checkbox.checked;
setError(<?php echo wp_json_encode( __( 'Update failed.', 'exelearning' ) ); ?>);
} else if (statusBox) {
statusBox.style.display = 'none';
}
}).catch(function () {
checkbox.checked = !checkbox.checked;
setError(<?php echo wp_json_encode( __( 'Network error.', 'exelearning' ) ); ?>);
});
});
})();
</script>
<?php
}

/**
* Persist the content-delivery asset-proxy toggle.
*
* Requires `manage_options` and a valid nonce, mirroring the style admin
* endpoints. Stores the flag in {@see ExeLearning_Content_Proxy::OPTION_PROXY_ASSETS}.
*/
public function ajax_toggle_proxy_assets() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'exelearning' ) ), 403 );
}
$nonce = isset( $_REQUEST['_ajax_nonce'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['_ajax_nonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, self::PROXY_ASSETS_NONCE ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid or missing security token.', 'exelearning' ) ), 403 );
}

// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
$raw = isset( $_POST['enabled'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['enabled'] ) ) : '';
$enabled = in_array( strtolower( $raw ), array( '1', 'true', 'on', 'yes' ), true );

update_option( ExeLearning_Content_Proxy::OPTION_PROXY_ASSETS, $enabled ? 1 : 0, false );
wp_send_json_success( array( 'enabled' => $enabled ) );
}

/**
* Render the style management section.
*
Expand Down
1 change: 1 addition & 0 deletions docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,4 @@ The plugin also exposes a few low-level configuration filters used as safety lim
| `exelearning_max_extract_bytes` | `1073741824` (1 GB) | Maximum uncompressed extraction size. |
| `exelearning_styles_max_zip_size` | `20 MB` | Maximum uploaded style ZIP size. |
| `exelearning_content_origin` | `''` | Origin URL used when serving extracted content. |
| `exelearning_proxy_assets` | `false` | Whether package assets (CSS, JS, fonts, images, media) are served through the WordPress content proxy with explicit `Content-Type` headers instead of being linked directly from the uploads directory. Overrides the **Content delivery** setting; useful to force the mode on for servers with an incorrect MIME configuration (e.g. JavaScript returned as `text/plain` with `nosniff`). |
49 changes: 48 additions & 1 deletion includes/class-content-proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
*/
class ExeLearning_Content_Proxy {

/**
* Option name for the optional asset-proxy mode (issue #53).
*
* When truthy, every package asset (CSS, JS, fonts, images, media…) is
* routed through this proxy so WordPress can send explicit Content-Type
* headers, instead of being linked directly from the uploads directory.
*
* @var string
*/
const OPTION_PROXY_ASSETS = 'exelearning_proxy_assets';

/**
* MIME types for common file extensions.
*
Expand All @@ -32,6 +43,7 @@ class ExeLearning_Content_Proxy {
'htm' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'mjs' => 'application/javascript',
'json' => 'application/json',
'xml' => 'application/xml',
'png' => 'image/png',
Expand Down Expand Up @@ -400,7 +412,42 @@ private static function is_html_path( $path ) {
private static function is_proxied_path( $path ) {
$clean_path = strtok( $path, '?#' );
$extension = strtolower( pathinfo( $clean_path, PATHINFO_EXTENSION ) );
return in_array( $extension, array( 'html', 'htm', 'svg', 'xml' ), true );

// Script-capable documents are always proxied for hardened headers.
if ( in_array( $extension, array( 'html', 'htm', 'svg', 'xml' ), true ) ) {
return true;
}

// Optional asset-proxy mode (issue #53): when enabled, route every
// package asset through the proxy so WordPress emits explicit
// Content-Type headers, working around servers that return the wrong
// MIME type (e.g. JavaScript served as text/plain with nosniff).
if ( '' !== $extension && self::is_asset_proxy_enabled() ) {
return true;
}

return false;
}

/**
* Whether the optional asset-proxy mode is enabled.
*
* Defaults to disabled, keeping direct uploads URLs for performance. The
* stored option can be overridden at runtime with the
* `exelearning_proxy_assets` filter, e.g. to force the mode on for a
* specific environment.
*
* @return bool
*/
public static function is_asset_proxy_enabled() {
$enabled = (bool) get_option( self::OPTION_PROXY_ASSETS, false );

/**
* Filter whether package assets are served through the WordPress proxy.
*
* @param bool $enabled Whether the asset-proxy mode is enabled.
*/
return (bool) apply_filters( 'exelearning_proxy_assets', $enabled );
}

/**
Expand Down
Binary file modified languages/exelearning-ca.mo
Binary file not shown.
Loading