Files
WooNooW/includes/Admin/Assets.php
dwindown 8312c18f64 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
2025-11-11 10:28:47 +07:00

251 lines
12 KiB
PHP

<?php
namespace WooNooW\Admin;
use WooNooW\Compat\MenuProvider;
use WooNooW\Compat\AddonRegistry;
use WooNooW\Compat\RouteRegistry;
use WooNooW\Compat\NavigationRegistry;
class Assets {
public static function init() {
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue']);
}
public static function enqueue($hook) {
if ($hook !== 'toplevel_page_woonoow') {
return;
}
// Decide dev vs prod
$is_dev = self::is_dev_mode();
if ($is_dev) {
self::enqueue_dev();
} else {
self::enqueue_prod();
}
}
/** ----------------------------------------
* DEV MODE (Vite dev server)
* -------------------------------------- */
private static function enqueue_dev(): void {
$dev_url = self::dev_server_url(); // e.g. http://localhost:5173
// 1) Create a small handle to attach config (window.WNW_API)
$handle = 'wnw-admin-dev-config';
wp_register_script($handle, '', [], null, true);
wp_enqueue_script($handle);
// Attach runtime config (before module loader runs)
// If you prefer, keep using self::localize_runtime($handle)
wp_localize_script($handle, 'WNW_API', [
'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'),
'isDev' => true,
'devServer' => $dev_url,
'adminScreen' => 'woonoow',
'adminUrl' => admin_url('admin.php'),
]);
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)
wp_localize_script($handle, 'wpApiSettings', [
'root' => untrailingslashit(esc_url_raw(rest_url())),
'nonce' => wp_create_nonce('wp_rest'),
]);
wp_add_inline_script($handle, 'window.wpApiSettings = window.wpApiSettings || wpApiSettings;', 'after');
// Also expose compact global for convenience
wp_localize_script($handle, 'wnw', [
'isDev' => true,
'devServer' => $dev_url,
'adminUrl' => admin_url('admin.php'),
'siteTitle' => get_bloginfo('name') ?: 'WooNooW',
]);
wp_add_inline_script($handle, 'window.wnw = window.wnw || wnw;', 'after');
// Localize store currency data (same as prod)
wp_localize_script($handle, 'WNW_STORE', self::store_runtime());
wp_add_inline_script($handle, 'window.WNW_STORE = window.WNW_STORE || WNW_STORE;', 'after');
// Localize Woo menus snapshot for instant render
$menus_snapshot = class_exists(MenuProvider::class) ? MenuProvider::get_snapshot() : [];
wp_localize_script($handle, 'WNW_WC_MENUS', ['items' => $menus_snapshot]);
wp_add_inline_script($handle, 'window.WNW_WC_MENUS = window.WNW_WC_MENUS || WNW_WC_MENUS;', 'after');
// Addon system data
wp_localize_script($handle, 'WNW_ADDONS', AddonRegistry::get_frontend_registry());
wp_add_inline_script($handle, 'window.WNW_ADDONS = window.WNW_ADDONS || WNW_ADDONS;', 'after');
wp_localize_script($handle, 'WNW_ADDON_ROUTES', RouteRegistry::get_frontend_routes());
wp_add_inline_script($handle, 'window.WNW_ADDON_ROUTES = window.WNW_ADDON_ROUTES || WNW_ADDON_ROUTES;', 'after');
wp_localize_script($handle, 'WNW_NAV_TREE', NavigationRegistry::get_frontend_nav_tree());
wp_add_inline_script($handle, 'window.WNW_NAV_TREE = window.WNW_NAV_TREE || WNW_NAV_TREE;', 'after');
// Temporary compat aliases for old WNM_*
wp_add_inline_script($handle, 'window.WNM_API = window.WNM_API || window.WNW_API;', 'after');
wp_add_inline_script($handle, 'window.WNM_WC_MENUS = window.WNM_WC_MENUS || window.WNW_WC_MENUS;', 'after');
// 2) Print a real module tag in the footer to load Vite client + app
add_action('admin_print_footer_scripts', function () use ($dev_url) {
// 1) React Refresh preamble (required by @vitejs/plugin-react)
?>
<script type="module">
import RefreshRuntime from "<?php echo esc_url( $dev_url ); ?>/@react-refresh";
RefreshRuntime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
window.__vite_plugin_react_preamble_installed__ = true;
</script>
<?php
// 2) Vite client (HMR)
printf('<script type="module" src="%s/@vite/client"></script>' . "\n", esc_url($dev_url));
// 3) Your app entry
printf('<script type="module">import "%s/src/main.tsx";</script>' . "\n", esc_url($dev_url));
}, 1);
}
/** ----------------------------------------
* PROD MODE (built assets in admin-spa/dist)
* -------------------------------------- */
private static function enqueue_prod(): void {
$dist_dir = plugin_dir_path(__FILE__) . '../admin-spa/dist/';
$base_url = plugins_url('../admin-spa/dist/', __FILE__);
$css = 'app.css';
$js = 'app.js';
$ver_css = file_exists($dist_dir . $css) ? (string) filemtime($dist_dir . $css) : self::asset_version();
$ver_js = file_exists($dist_dir . $js) ? (string) filemtime($dist_dir . $js) : self::asset_version();
if (file_exists($dist_dir . $css)) {
wp_enqueue_style('wnw-admin', $base_url . $css, [], $ver_css);
}
if (file_exists($dist_dir . $js)) {
wp_enqueue_script('wnw-admin', $base_url . $js, ['wp-element'], $ver_js, true);
self::localize_runtime('wnw-admin');
}
}
/** Attach runtime config to a handle */
private static function localize_runtime(string $handle): void {
wp_localize_script($handle, 'WNW_API', [
'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
'nonce' => wp_create_nonce('wp_rest'),
'isDev' => self::is_dev_mode(),
'devServer' => self::dev_server_url(),
'adminScreen' => 'woonoow',
'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)
wp_localize_script($handle, 'wpApiSettings', [
'root' => untrailingslashit(esc_url_raw(rest_url())),
'nonce' => wp_create_nonce('wp_rest'),
]);
wp_localize_script($handle, 'WNW_STORE', self::store_runtime());
wp_add_inline_script($handle, 'window.WNW_STORE = window.WNW_STORE || WNW_STORE;', 'after');
// Compact global (prod)
wp_localize_script($handle, 'wnw', [
'isDev' => (bool) self::is_dev_mode(),
'devServer' => (string) self::dev_server_url(),
'adminUrl' => admin_url('admin.php'),
'siteTitle' => get_bloginfo('name') ?: 'WooNooW',
]);
wp_add_inline_script($handle, 'window.wnw = window.wnw || wnw;', 'after');
// Menus snapshot (prod)
$menus_snapshot = class_exists(MenuProvider::class) ? MenuProvider::get_snapshot() : [];
wp_localize_script($handle, 'WNW_WC_MENUS', ['items' => $menus_snapshot]);
wp_add_inline_script($handle, 'window.WNW_WC_MENUS = window.WNW_WC_MENUS || WNW_WC_MENUS;', 'after');
// Addon system data (prod)
wp_localize_script($handle, 'WNW_ADDONS', AddonRegistry::get_frontend_registry());
wp_add_inline_script($handle, 'window.WNW_ADDONS = window.WNW_ADDONS || WNW_ADDONS;', 'after');
wp_localize_script($handle, 'WNW_ADDON_ROUTES', RouteRegistry::get_frontend_routes());
wp_add_inline_script($handle, 'window.WNW_ADDON_ROUTES = window.WNW_ADDON_ROUTES || WNW_ADDON_ROUTES;', 'after');
wp_localize_script($handle, 'WNW_NAV_TREE', NavigationRegistry::get_frontend_nav_tree());
wp_add_inline_script($handle, 'window.WNW_NAV_TREE = window.WNW_NAV_TREE || WNW_NAV_TREE;', 'after');
// Temporary compat aliases for old WNM_*
wp_add_inline_script($handle, 'window.WNM_API = window.WNM_API || window.WNW_API;', 'after');
wp_add_inline_script($handle, 'window.WNM_WC_MENUS = window.WNM_WC_MENUS || window.WNW_WC_MENUS;', 'after');
}
/** Runtime store meta for frontend (currency, decimals, separators, position). */
private static function store_runtime(): array {
// WooCommerce helpers may not exist in some contexts; guard with defaults
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';
$currency_sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol($currency) : '$';
$decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
$thousand_sep = function_exists('wc_get_price_thousand_separator') ? wc_get_price_thousand_separator() : ',';
$decimal_sep = function_exists('wc_get_price_decimal_separator') ? wc_get_price_decimal_separator() : '.';
$currency_pos = function_exists('get_option') ? get_option('woocommerce_currency_pos', 'left') : 'left';
return [
'currency' => $currency,
'currency_symbol' => $currency_sym,
'decimals' => (int) $decimals,
'thousand_sep' => (string) $thousand_sep,
'decimal_sep' => (string) $decimal_sep,
'currency_pos' => (string) $currency_pos,
];
}
/** Determine dev mode:
* - WP environment 'development'
* - or constant WOONOOW_ADMIN_DEV=true
* - or filter override (woonoow/admin_is_dev)
*/
private static function is_dev_mode(): bool {
$env_is_dev = function_exists('wp_get_environment_type') && wp_get_environment_type() === 'development';
$const_dev = defined('WOONOOW_ADMIN_DEV') && WOONOOW_ADMIN_DEV === true;
$is_dev = $env_is_dev || $const_dev;
/**
* Filter: force dev/prod mode for WooNooW admin assets.
* Return true to use Vite dev server, false to use built assets.
*/
return (bool) apply_filters('woonoow/admin_is_dev', $is_dev);
}
/** Dev server URL (filterable) */
private static function dev_server_url(): string {
$default = 'http://localhost:5173';
/** Filter: change dev server URL if needed */
return (string) apply_filters('woonoow/admin_dev_server', $default);
}
/** Basic asset versioning */
private static function asset_version(): string {
// Bump when releasing; in dev we don't cache-bust
return defined('WOONOOW_VERSION') ? WOONOOW_VERSION : '0.1.0';
}
}