Skip to content

Commit 58921da

Browse files
committed
Fix url
1 parent 7d3b031 commit 58921da

3 files changed

Lines changed: 106 additions & 34 deletions

File tree

includes/class-content-proxy.php

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ private function serve_css_with_rewritten_urls( $full_path, $hash, $mime_type, $
258258
return;
259259
}
260260

261-
$base_url = rest_url( 'exelearning/v1/content/' . $hash . '/' );
261+
$base_url = self::get_uploads_url( $hash );
262262

263263
// Get the directory of the current CSS file for resolving relative paths.
264264
$current_dir = '';
@@ -301,7 +301,8 @@ function ( $matches ) use ( $base_url, $current_dir ) {
301301
* @return string Modified HTML with absolute URLs.
302302
*/
303303
private function rewrite_relative_urls( $html, $hash, $file_path = '' ) {
304-
$base_url = rest_url( 'exelearning/v1/content/' . $hash . '/' );
304+
$uploads_url = self::get_uploads_url( $hash );
305+
$proxy_url = self::get_proxy_url( $hash, '' );
305306

306307
// Get the directory of the current file for resolving relative paths.
307308
$current_dir = '';
@@ -325,7 +326,7 @@ private function rewrite_relative_urls( $html, $hash, $file_path = '' ) {
325326
foreach ( $patterns as $pattern ) {
326327
$html = preg_replace_callback(
327328
$pattern,
328-
function ( $matches ) use ( $base_url, $current_dir ) {
329+
function ( $matches ) use ( $uploads_url, $proxy_url, $current_dir ) {
329330
$prefix = $matches[1];
330331
$attr = $matches[2];
331332
$url = $matches[3];
@@ -339,33 +340,47 @@ function ( $matches ) use ( $base_url, $current_dir ) {
339340
// Resolve the relative URL based on current directory.
340341
$resolved_path = $this->resolve_relative_path( $current_dir, $url );
341342

342-
// Build absolute URL.
343-
$absolute_url = $base_url . $resolved_path;
343+
// HTML files go through the proxy (for CSP headers);
344+
// all other assets are served directly from uploads.
345+
$base_url = self::is_html_path( $resolved_path ) ? $proxy_url : $uploads_url;
344346

345-
return $prefix . $attr . esc_url( $absolute_url ) . $end_quote;
347+
return $prefix . $attr . esc_url( $base_url . $resolved_path ) . $end_quote;
346348
},
347349
$html
348350
);
349351
}
350352

351-
// Also handle url() in inline styles.
353+
// Also handle url() in inline styles (never HTML, always assets).
352354
$html = preg_replace_callback(
353355
'/url\s*\(\s*["\']?(?!https?:\/\/|data:|\/\/|#)([^"\')\s]+)["\']?\s*\)/i',
354-
function ( $matches ) use ( $base_url, $current_dir ) {
356+
function ( $matches ) use ( $uploads_url, $current_dir ) {
355357
$url = $matches[1];
356358
if ( empty( $url ) || '/' === $url[0] ) {
357359
return $matches[0];
358360
}
359361
// Resolve the relative URL based on current directory.
360362
$resolved_path = $this->resolve_relative_path( $current_dir, $url );
361-
return 'url("' . esc_url( $base_url . $resolved_path ) . '")';
363+
return 'url("' . esc_url( $uploads_url . $resolved_path ) . '")';
362364
},
363365
$html
364366
);
365367

366368
return $html;
367369
}
368370

371+
/**
372+
* Check if a file path points to an HTML file.
373+
*
374+
* @param string $path File path to check.
375+
* @return bool True if the path ends with .html or .htm.
376+
*/
377+
private static function is_html_path( $path ) {
378+
// Strip query string and fragment before checking extension.
379+
$clean_path = strtok( $path, '?#' );
380+
$extension = strtolower( pathinfo( $clean_path, PATHINFO_EXTENSION ) );
381+
return 'html' === $extension || 'htm' === $extension;
382+
}
383+
369384
/**
370385
* Resolve a relative path against a base directory.
371386
*
@@ -501,4 +516,20 @@ public static function get_proxy_url( $hash, $file = 'index.html' ) {
501516
}
502517
return rest_url( 'exelearning/v1/content/' . $hash . '/' . $file );
503518
}
519+
520+
/**
521+
* Generate a direct uploads URL for the given hash and file.
522+
*
523+
* Sub-assets (CSS, JS, images, fonts) are served directly from the uploads
524+
* directory to avoid 404s on hosted environments where the web server
525+
* intercepts requests with static file extensions.
526+
*
527+
* @param string $hash Extraction hash.
528+
* @param string $file File path (default: empty).
529+
* @return string Uploads URL.
530+
*/
531+
public static function get_uploads_url( $hash, $file = '' ) {
532+
$upload_dir = wp_upload_dir();
533+
return trailingslashit( $upload_dir['baseurl'] ) . 'exelearning/' . $hash . '/' . $file;
534+
}
504535
}

includes/class-elp-upload-handler.php

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -189,41 +189,51 @@ public function exelearning_delete_extracted_folder( $post_id ) {
189189
}
190190

191191
/**
192-
* Creates a security .htaccess file to block direct access to extracted content.
192+
* Creates a security .htaccess file to control direct access to extracted content.
193193
*
194-
* All content must be served through the secure proxy controller.
194+
* HTML files are blocked (must be served through the secure proxy for CSP headers).
195+
* Static assets (CSS, JS, images, fonts, media) are allowed for direct serving,
196+
* which avoids 403/404 errors on hosted environments where the web server
197+
* intercepts requests with static file extensions.
195198
*/
196199
private function create_security_htaccess() {
197200
$upload_dir = wp_upload_dir();
198201
$htaccess_path = trailingslashit( $upload_dir['basedir'] ) . 'exelearning/.htaccess';
199202

200-
// Only create if it doesn't exist.
201-
if ( file_exists( $htaccess_path ) ) {
202-
return;
203-
}
204-
205203
$htaccess_content = <<<'HTACCESS'
206-
# Security: Block direct access to eXeLearning extracted content
207-
# All content must be served through the secure proxy controller
204+
# Security: Control direct access to eXeLearning extracted content
205+
# HTML files must be served through the secure proxy controller (for CSP headers)
206+
# Static assets (CSS, JS, images, fonts, media) are allowed for direct serving
208207
209-
# Deny all direct access
210-
<IfModule mod_authz_core.c>
211-
# Apache 2.4+
212-
Require all denied
213-
</IfModule>
214-
<IfModule !mod_authz_core.c>
215-
# Apache 2.2
216-
Order deny,allow
217-
Deny from all
218-
</IfModule>
219-
220-
# Alternative: return 403 for all requests
221208
<IfModule mod_rewrite.c>
222209
RewriteEngine On
210+
211+
# Allow static assets to be served directly
212+
RewriteCond %{REQUEST_URI} \.(css|js|json|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot|otf|mp[34]|webm|og[gv]|wav|pdf|zip|txt|xml)$ [NC]
213+
RewriteRule ^ - [L]
214+
215+
# Block direct access to everything else (HTML files, etc.)
223216
RewriteRule ^ - [F,L]
224217
</IfModule>
218+
219+
<IfModule !mod_rewrite.c>
220+
# Fallback without mod_rewrite: deny all (proxy will still work)
221+
<IfModule mod_authz_core.c>
222+
Require all denied
223+
</IfModule>
224+
<IfModule !mod_authz_core.c>
225+
Order deny,allow
226+
Deny from all
227+
</IfModule>
228+
</IfModule>
225229
HTACCESS;
226230

231+
// Only write if the file doesn't exist or its content has changed.
232+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
233+
if ( file_exists( $htaccess_path ) && file_get_contents( $htaccess_path ) === $htaccess_content ) {
234+
return;
235+
}
236+
227237
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
228238
file_put_contents( $htaccess_path, $htaccess_content );
229239
}

tests/unit/ContentProxyTest.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ public function test_rewrite_relative_urls_basic() {
664664
$hash = str_repeat( 'a', 40 );
665665
$result = $method->invoke( $this->proxy, $html, $hash, '' );
666666

667-
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
667+
$this->assertStringContainsString( 'uploads/exelearning/', $result );
668668
$this->assertStringContainsString( 'images/logo.png', $result );
669669
}
670670

@@ -707,7 +707,7 @@ public function test_rewrite_relative_urls_handles_href() {
707707
$hash = str_repeat( 'a', 40 );
708708
$result = $method->invoke( $this->proxy, $html, $hash, '' );
709709

710-
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
710+
$this->assertStringContainsString( 'uploads/exelearning/', $result );
711711
$this->assertStringContainsString( 'css/style.css', $result );
712712
}
713713

@@ -722,7 +722,7 @@ public function test_rewrite_relative_urls_handles_poster() {
722722
$hash = str_repeat( 'a', 40 );
723723
$result = $method->invoke( $this->proxy, $html, $hash, '' );
724724

725-
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
725+
$this->assertStringContainsString( 'uploads/exelearning/', $result );
726726
$this->assertStringContainsString( 'thumbnails/video.jpg', $result );
727727
}
728728

@@ -737,7 +737,7 @@ public function test_rewrite_relative_urls_handles_inline_style() {
737737
$hash = str_repeat( 'a', 40 );
738738
$result = $method->invoke( $this->proxy, $html, $hash, '' );
739739

740-
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
740+
$this->assertStringContainsString( 'uploads/exelearning/', $result );
741741
$this->assertStringContainsString( 'images/bg.png', $result );
742742
}
743743

@@ -854,6 +854,37 @@ public function test_rewrite_relative_urls_absolute_path() {
854854
$this->assertEquals( $html, $result );
855855
}
856856

857+
/**
858+
* Test rewrite_relative_urls routes HTML links through proxy.
859+
*/
860+
public function test_rewrite_relative_urls_html_links_use_proxy() {
861+
$method = new ReflectionMethod( ExeLearning_Content_Proxy::class, 'rewrite_relative_urls' );
862+
$method->setAccessible( true );
863+
864+
$html = '<a href="html/page2.html">Next</a>';
865+
$hash = str_repeat( 'a', 40 );
866+
$result = $method->invoke( $this->proxy, $html, $hash, '' );
867+
868+
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
869+
$this->assertStringContainsString( 'html/page2.html', $result );
870+
$this->assertStringNotContainsString( 'uploads/exelearning/', $result );
871+
}
872+
873+
/**
874+
* Test rewrite_relative_urls routes HTM links through proxy.
875+
*/
876+
public function test_rewrite_relative_urls_htm_links_use_proxy() {
877+
$method = new ReflectionMethod( ExeLearning_Content_Proxy::class, 'rewrite_relative_urls' );
878+
$method->setAccessible( true );
879+
880+
$html = '<a href="page.htm">Page</a>';
881+
$hash = str_repeat( 'a', 40 );
882+
$result = $method->invoke( $this->proxy, $html, $hash, '' );
883+
884+
$this->assertStringContainsString( 'exelearning/v1/content/', $result );
885+
$this->assertStringNotContainsString( 'uploads/exelearning/', $result );
886+
}
887+
857888
/**
858889
* Test validate_hash with valid hash.
859890
*/

0 commit comments

Comments
 (0)