fix: Standalone nav + REST URL + SVG upload support

##  Issue 1: Standalone Mode Navigation
**Problem:** Standalone mode not getting WNW_NAV_TREE from PHP
**Fixed:** Added WNW_NAV_TREE injection to StandaloneAdmin.php
**Result:** Navigation now works in standalone mode with PHP as single source

##  Issue 2: 404 Errors for branding and customer-settings
**Problem:** REST URLs had trailing slashes causing double slashes
**Root Cause:**
- `rest_url("woonoow/v1")` returns `https://site.com/wp-json/woonoow/v1/`
- Frontend: `restUrl + "/store/branding"` = double slash
- WP-admin missing WNW_CONFIG entirely

**Fixed:**
1. **Removed trailing slashes** from all REST URLs using `untrailingslashit()`
   - StandaloneAdmin.php
   - Assets.php (dev and prod modes)

2. **Added WNW_CONFIG to wp-admin** for API compatibility
   - Dev mode: Added WNW_CONFIG with restUrl, nonce, standaloneMode, etc.
   - Prod mode: Added WNW_CONFIG to localize_runtime()
   - Now both modes use same config structure

**Result:**
-  `/store/branding` works in all modes
-  `/store/customer-settings` works in all modes
-  Consistent API access across standalone and wp-admin

##  Issue 3: SVG Upload Error 500
**Problem:** WordPress blocks SVG uploads by default
**Security:** "Sorry, you are not allowed to upload this file type"

**Fixed:** Created MediaUpload.php with:
1. **Allow SVG uploads** for users with upload_files capability
2. **Fix SVG mime type detection** (WordPress issue)
3. **Sanitize SVG on upload** - reject files with:
   - `<script>` tags
   - `javascript:` protocols
   - Event handlers (onclick, onload, etc.)

**Result:**
-  SVG uploads work securely
-  Dangerous SVG content blocked
-  Only authorized users can upload

---

## Files Modified:
- `StandaloneAdmin.php` - Add nav tree + fix REST URL
- `Assets.php` - Add WNW_CONFIG + fix REST URLs
- `Bootstrap.php` - Initialize MediaUpload
- `MediaUpload.php` - NEW: SVG upload support with security

## Testing:
1.  Navigation works in standalone mode
2.  Branding endpoint works in all modes
3.  Customer settings endpoint works in all modes
4.  SVG logo upload works
5.  Dangerous SVG files rejected
This commit is contained in:
dwindown
2025-11-11 10:28:47 +07:00
parent 677c04dd62
commit 8312c18f64
4 changed files with 136 additions and 5 deletions

View File

@@ -39,7 +39,7 @@ class Assets {
// Attach runtime config (before module loader runs) // Attach runtime config (before module loader runs)
// If you prefer, keep using self::localize_runtime($handle) // If you prefer, keep using self::localize_runtime($handle)
wp_localize_script($handle, 'WNW_API', [ wp_localize_script($handle, 'WNW_API', [
'root' => esc_url_raw(rest_url('woonoow/v1/')), 'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
'isDev' => true, 'isDev' => true,
'devServer' => $dev_url, 'devServer' => $dev_url,
@@ -48,9 +48,19 @@ class Assets {
]); ]);
wp_add_inline_script($handle, 'window.WNW_API = window.WNW_API || WNW_API;', 'after'); wp_add_inline_script($handle, 'window.WNW_API = window.WNW_API || WNW_API;', 'after');
// WNW_CONFIG for compatibility with standalone mode code
wp_localize_script($handle, 'WNW_CONFIG', [
'restUrl' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'),
'standaloneMode' => false,
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
'isAuthenticated' => is_user_logged_in(),
]);
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
// WordPress REST API settings (for media upload compatibility) // WordPress REST API settings (for media upload compatibility)
wp_localize_script($handle, 'wpApiSettings', [ wp_localize_script($handle, 'wpApiSettings', [
'root' => esc_url_raw(rest_url()), 'root' => untrailingslashit(esc_url_raw(rest_url())),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
]); ]);
wp_add_inline_script($handle, 'window.wpApiSettings = window.wpApiSettings || wpApiSettings;', 'after'); wp_add_inline_script($handle, 'window.wpApiSettings = window.wpApiSettings || wpApiSettings;', 'after');
@@ -134,7 +144,7 @@ class Assets {
/** Attach runtime config to a handle */ /** Attach runtime config to a handle */
private static function localize_runtime(string $handle): void { private static function localize_runtime(string $handle): void {
wp_localize_script($handle, 'WNW_API', [ wp_localize_script($handle, 'WNW_API', [
'root' => esc_url_raw(rest_url('woonoow/v1/')), 'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
'isDev' => self::is_dev_mode(), 'isDev' => self::is_dev_mode(),
'devServer' => self::dev_server_url(), 'devServer' => self::dev_server_url(),
@@ -142,9 +152,18 @@ class Assets {
'adminUrl' => admin_url('admin.php'), 'adminUrl' => admin_url('admin.php'),
]); ]);
// WNW_CONFIG for compatibility with standalone mode code
wp_localize_script($handle, 'WNW_CONFIG', [
'restUrl' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'),
'standaloneMode' => false,
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
'isAuthenticated' => is_user_logged_in(),
]);
// WordPress REST API settings (for media upload compatibility) // WordPress REST API settings (for media upload compatibility)
wp_localize_script($handle, 'wpApiSettings', [ wp_localize_script($handle, 'wpApiSettings', [
'root' => esc_url_raw(rest_url()), 'root' => untrailingslashit(esc_url_raw(rest_url())),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
]); ]);

View File

@@ -57,7 +57,7 @@ class StandaloneAdmin {
// Get nonce for REST API // Get nonce for REST API
$nonce = wp_create_nonce( 'wp_rest' ); $nonce = wp_create_nonce( 'wp_rest' );
$rest_url = rest_url( 'woonoow/v1' ); $rest_url = untrailingslashit( rest_url( 'woonoow/v1' ) );
$wp_admin_url = admin_url( 'admin.php?page=woonoow' ); $wp_admin_url = admin_url( 'admin.php?page=woonoow' );
// Get current user data if authenticated // Get current user data if authenticated
@@ -134,6 +134,9 @@ class StandaloneAdmin {
// WooCommerce store settings (currency, formatting, etc.) // WooCommerce store settings (currency, formatting, etc.)
window.WNW_STORE = <?php echo wp_json_encode( $store_settings ); ?>; window.WNW_STORE = <?php echo wp_json_encode( $store_settings ); ?>;
// Navigation tree (single source of truth from PHP)
window.WNW_NAV_TREE = <?php echo wp_json_encode( \WooNooW\Compat\NavigationRegistry::get_frontend_nav_tree() ); ?>;
</script> </script>
<script type="module" src="<?php echo esc_url( $js_url ); ?>"></script> <script type="module" src="<?php echo esc_url( $js_url ); ?>"></script>

View File

@@ -18,6 +18,7 @@ use WooNooW\Api\Routes;
use WooNooW\Core\Mail\MailQueue; use WooNooW\Core\Mail\MailQueue;
use WooNooW\Core\Mail\WooEmailOverride; use WooNooW\Core\Mail\WooEmailOverride;
use WooNooW\Core\DataStores\OrderStore; use WooNooW\Core\DataStores\OrderStore;
use WooNooW\Core\MediaUpload;
use WooNooW\Branding; use WooNooW\Branding;
class Bootstrap { class Bootstrap {
@@ -28,6 +29,7 @@ class Bootstrap {
Assets::init(); Assets::init();
StandaloneAdmin::init(); StandaloneAdmin::init();
Branding::init(); Branding::init();
MediaUpload::init();
// Addon system (order matters: Registry → Routes → Navigation) // Addon system (order matters: Registry → Routes → Navigation)
AddonRegistry::init(); AddonRegistry::init();

View File

@@ -0,0 +1,107 @@
<?php
/**
* Media Upload Handler
*
* Handles file uploads including SVG support with security checks.
*
* @package WooNooW
*/
namespace WooNooW\Core;
class MediaUpload {
/**
* Initialize media upload hooks
*/
public static function init() {
// Allow SVG uploads
add_filter( 'upload_mimes', [ __CLASS__, 'allow_svg_upload' ] );
// Fix SVG mime type detection
add_filter( 'wp_check_filetype_and_ext', [ __CLASS__, 'fix_svg_mime_type' ], 10, 5 );
// Sanitize SVG on upload
add_filter( 'wp_handle_upload_prefilter', [ __CLASS__, 'sanitize_svg_upload' ] );
}
/**
* Allow SVG file uploads
*
* @param array $mimes Allowed mime types
* @return array Modified mime types
*/
public static function allow_svg_upload( $mimes ) {
// Only allow for users with upload_files capability
if ( ! current_user_can( 'upload_files' ) ) {
return $mimes;
}
$mimes['svg'] = 'image/svg+xml';
$mimes['svgz'] = 'image/svg+xml';
return $mimes;
}
/**
* Fix SVG mime type detection
*
* @param array $data File data
* @param string $file File path
* @param string $filename File name
* @param array $mimes Allowed mimes
* @param string $real_mime Real mime type
* @return array Modified file data
*/
public static function fix_svg_mime_type( $data, $file, $filename, $mimes, $real_mime ) {
if ( ! $data['ext'] && ! $data['type'] ) {
$wp_filetype = wp_check_filetype( $filename, $mimes );
$ext = $wp_filetype['ext'];
$type = $wp_filetype['type'];
if ( $ext === 'svg' || $ext === 'svgz' ) {
$data['ext'] = $ext;
$data['type'] = $type;
}
}
return $data;
}
/**
* Sanitize SVG files on upload
*
* @param array $file Upload file data
* @return array Modified file data
*/
public static function sanitize_svg_upload( $file ) {
// Only process SVG files
if ( $file['type'] !== 'image/svg+xml' ) {
return $file;
}
// Read file content
$svg_content = file_get_contents( $file['tmp_name'] );
if ( $svg_content === false ) {
$file['error'] = __( 'Could not read SVG file', 'woonoow' );
return $file;
}
// Basic security check - reject if contains script tags or event handlers
$dangerous_patterns = [
'/<script/i',
'/javascript:/i',
'/on\w+\s*=/i', // onclick, onload, etc.
];
foreach ( $dangerous_patterns as $pattern ) {
if ( preg_match( $pattern, $svg_content ) ) {
$file['error'] = __( 'SVG file contains potentially dangerous content', 'woonoow' );
return $file;
}
}
return $file;
}
}