Files
WooNooW/includes/Admin/StandaloneAdmin.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

176 lines
5.7 KiB
PHP

<?php
namespace WooNooW\Admin;
/**
* Standalone Admin Handler
*
* Handles /admin requests without requiring .htaccess modifications.
* Uses WordPress template_redirect hook to catch requests early.
*
* @package WooNooW\Admin
*/
class StandaloneAdmin {
/**
* Initialize standalone admin handler
*/
public static function init() {
// Catch /admin requests very early (before WordPress routing)
add_action( 'parse_request', [ __CLASS__, 'handle_admin_request' ], 1 );
}
/**
* Handle /admin requests
*/
public static function handle_admin_request() {
// Check if this is an /admin request
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
// Remove query string
$path = strtok( $request_uri, '?' );
// Only handle exact /admin or /admin/ paths (not asset files)
if ( $path !== '/admin' && $path !== '/admin/' ) {
return;
}
// This is a standalone admin request
self::render_standalone_admin();
exit;
}
/**
* Render standalone admin interface
*/
private static function render_standalone_admin() {
// Check if user is logged in and has permissions
$is_logged_in = is_user_logged_in();
$has_permission = $is_logged_in && current_user_can( 'manage_woocommerce' );
$is_authenticated = $is_logged_in && $has_permission;
// Debug logging (only in WP_DEBUG mode)
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( '[StandaloneAdmin] is_user_logged_in: ' . ( $is_logged_in ? 'true' : 'false' ) );
error_log( '[StandaloneAdmin] has manage_woocommerce: ' . ( $has_permission ? 'true' : 'false' ) );
error_log( '[StandaloneAdmin] is_authenticated: ' . ( $is_authenticated ? 'true' : 'false' ) );
}
// Get nonce for REST API
$nonce = wp_create_nonce( 'wp_rest' );
$rest_url = untrailingslashit( rest_url( 'woonoow/v1' ) );
$wp_admin_url = admin_url( 'admin.php?page=woonoow' );
// Get current user data if authenticated
$current_user = null;
if ( $is_authenticated ) {
$user = wp_get_current_user();
$current_user = [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email,
'avatar' => get_avatar_url( $user->ID ),
];
}
// Get WooCommerce store settings
$store_settings = self::get_store_settings();
// Get asset URLs
$plugin_url = plugins_url( '', dirname( dirname( __FILE__ ) ) );
$asset_url = $plugin_url . '/admin-spa/dist';
// Cache busting
$version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : '1.0.0';
$css_url = $asset_url . '/app.css?ver=' . $version;
$js_url = $asset_url . '/app.js?ver=' . $version;
// Render HTML
?>
<!DOCTYPE html>
<html lang="<?php echo esc_attr( get_locale() ); ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title><?php echo esc_html( get_option( 'blogname', 'WooNooW' ) ); ?> Admin</title>
<?php
// Favicon
$icon = get_option( 'woonoow_store_icon', '' );
if ( ! empty( $icon ) ) {
?>
<link rel="icon" type="image/png" href="<?php echo esc_url( $icon ); ?>" />
<link rel="apple-touch-icon" href="<?php echo esc_url( $icon ); ?>" />
<?php
}
?>
<!-- WooNooW Assets Only - NO wp_head() -->
<link rel="stylesheet" href="<?php echo esc_url( $css_url ); ?>">
</head>
<body class="woonoow-standalone">
<div id="woonoow-admin-app"></div>
<script>
// Minimal config - no WordPress bloat
window.WNW_CONFIG = {
restUrl: <?php echo wp_json_encode( $rest_url ); ?>,
nonce: <?php echo wp_json_encode( $nonce ); ?>,
standaloneMode: true,
wpAdminUrl: <?php echo wp_json_encode( $wp_admin_url ); ?>,
isAuthenticated: <?php echo $is_authenticated ? 'true' : 'false'; ?>,
currentUser: <?php echo wp_json_encode( $current_user ); ?>,
locale: <?php echo wp_json_encode( get_locale() ); ?>,
siteUrl: <?php echo wp_json_encode( home_url() ); ?>,
siteName: <?php echo wp_json_encode( get_bloginfo( 'name' ) ); ?>
};
// Also set WNW_API for API compatibility
window.WNW_API = {
root: <?php echo wp_json_encode( $rest_url ); ?>,
nonce: <?php echo wp_json_encode( $nonce ); ?>,
isDev: <?php echo ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'true' : 'false'; ?>
};
// WooCommerce store settings (currency, formatting, etc.)
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 type="module" src="<?php echo esc_url( $js_url ); ?>"></script>
<?php
// NO wp_footer() - we don't want theme/plugin scripts
?>
</body>
</html>
<?php
}
/**
* Get WooCommerce store settings for frontend
*
* @return array Store settings (currency, decimals, separators, etc.)
*/
private static function get_store_settings(): array {
// Get WooCommerce settings with fallbacks
$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 = get_option( 'woocommerce_currency_pos', '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,
];
}
}