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:
@@ -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'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
107
includes/Core/MediaUpload.php
Normal file
107
includes/Core/MediaUpload.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user