fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Compat;
|
||||
|
||||
if ( ! defined('ABSPATH') ) exit;
|
||||
if (! defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* Navigation Registry
|
||||
@@ -11,36 +12,39 @@ if ( ! defined('ABSPATH') ) exit;
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class NavigationRegistry {
|
||||
class NavigationRegistry
|
||||
{
|
||||
const NAV_OPTION = 'wnw_nav_tree';
|
||||
const NAV_VERSION = '1.3.0'; // Added Subscriptions section
|
||||
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
// Use 'init' hook instead of 'plugins_loaded' to avoid translation loading warnings (WP 6.7+)
|
||||
add_action('init', [__CLASS__, 'build_nav_tree'], 10);
|
||||
add_action('activated_plugin', [__CLASS__, 'flush']);
|
||||
add_action('deactivated_plugin', [__CLASS__, 'flush']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build the complete navigation tree
|
||||
*/
|
||||
public static function build_nav_tree() {
|
||||
public static function build_nav_tree()
|
||||
{
|
||||
// Check if we need to rebuild (version mismatch)
|
||||
$cached = get_option(self::NAV_OPTION, []);
|
||||
$cached_version = $cached['version'] ?? '';
|
||||
|
||||
|
||||
if ($cached_version === self::NAV_VERSION && !empty($cached['tree'])) {
|
||||
// Cache is valid, no need to rebuild
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Base navigation tree (core WooNooW sections)
|
||||
$tree = self::get_base_tree();
|
||||
|
||||
|
||||
/**
|
||||
* Filter: woonoow/nav_tree
|
||||
*
|
||||
@@ -64,7 +68,7 @@ class NavigationRegistry {
|
||||
* });
|
||||
*/
|
||||
$tree = apply_filters('woonoow/nav_tree', $tree);
|
||||
|
||||
|
||||
// Allow per-section modification
|
||||
foreach ($tree as &$section) {
|
||||
$key = $section['key'] ?? '';
|
||||
@@ -90,7 +94,7 @@ class NavigationRegistry {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Store in option
|
||||
update_option(self::NAV_OPTION, [
|
||||
'version' => self::NAV_VERSION,
|
||||
@@ -98,13 +102,14 @@ class NavigationRegistry {
|
||||
'updated' => time(),
|
||||
], false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get base navigation tree (core sections)
|
||||
*
|
||||
* @return array Base navigation tree
|
||||
*/
|
||||
private static function get_base_tree(): array {
|
||||
private static function get_base_tree(): array
|
||||
{
|
||||
$tree = [
|
||||
[
|
||||
'key' => 'dashboard',
|
||||
@@ -198,37 +203,39 @@ class NavigationRegistry {
|
||||
'children' => [], // Empty array = no submenu bar
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get marketing submenu children
|
||||
*
|
||||
* @return array Marketing submenu items
|
||||
*/
|
||||
private static function get_marketing_children(): array {
|
||||
private static function get_marketing_children(): array
|
||||
{
|
||||
$children = [];
|
||||
|
||||
|
||||
// Newsletter - only if module enabled
|
||||
if (\WooNooW\Core\ModuleRegistry::is_enabled('newsletter')) {
|
||||
$children[] = ['label' => __('Newsletter', 'woonoow'), 'mode' => 'spa', 'path' => '/marketing/newsletter'];
|
||||
}
|
||||
|
||||
|
||||
// Coupons - always available
|
||||
$children[] = ['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons'];
|
||||
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get settings submenu children
|
||||
*
|
||||
* @return array Settings submenu items
|
||||
*/
|
||||
private static function get_settings_children(): array {
|
||||
private static function get_settings_children(): array
|
||||
{
|
||||
$admin = admin_url('admin.php');
|
||||
|
||||
|
||||
$children = [
|
||||
// Core Settings (Shopify-inspired)
|
||||
['label' => __('Store Details', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/store'],
|
||||
@@ -236,25 +243,27 @@ class NavigationRegistry {
|
||||
['label' => __('Shipping & Delivery', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/shipping'],
|
||||
['label' => __('Tax', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/tax'],
|
||||
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customers'],
|
||||
['label' => __('Security', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/security'],
|
||||
['label' => __('Notifications', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/notifications'],
|
||||
['label' => __('Modules', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/modules'],
|
||||
['label' => __('Developer', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/developer'],
|
||||
];
|
||||
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get subscriptions navigation section
|
||||
* Returns empty array if module is not enabled
|
||||
*
|
||||
* @return array Subscriptions section or empty array
|
||||
*/
|
||||
private static function get_subscriptions_section(): array {
|
||||
private static function get_subscriptions_section(): array
|
||||
{
|
||||
if (!\WooNooW\Core\ModuleRegistry::is_enabled('subscription')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'subscriptions',
|
||||
@@ -267,24 +276,26 @@ class NavigationRegistry {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the complete navigation tree
|
||||
*
|
||||
* @return array Navigation tree
|
||||
*/
|
||||
public static function get_nav_tree(): array {
|
||||
public static function get_nav_tree(): array
|
||||
{
|
||||
$data = get_option(self::NAV_OPTION, []);
|
||||
return $data['tree'] ?? self::get_base_tree();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a specific section by key
|
||||
*
|
||||
* @param string $key Section key
|
||||
* @return array|null Section data or null if not found
|
||||
*/
|
||||
public static function get_section(string $key): ?array {
|
||||
public static function get_section(string $key): ?array
|
||||
{
|
||||
$tree = self::get_nav_tree();
|
||||
foreach ($tree as $section) {
|
||||
if (($section['key'] ?? '') === $key) {
|
||||
@@ -293,22 +304,24 @@ class NavigationRegistry {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flush navigation cache
|
||||
*/
|
||||
public static function flush() {
|
||||
public static function flush()
|
||||
{
|
||||
delete_option(self::NAV_OPTION);
|
||||
// Rebuild immediately after flush
|
||||
self::build_nav_tree();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get navigation tree for frontend
|
||||
*
|
||||
* @return array Array suitable for JSON encoding
|
||||
*/
|
||||
public static function get_frontend_nav_tree(): array {
|
||||
public static function get_frontend_nav_tree(): array
|
||||
{
|
||||
return self::get_nav_tree();
|
||||
}
|
||||
}
|
||||
|
||||
301
includes/Compat/SecuritySettingsProvider.php
Normal file
301
includes/Compat/SecuritySettingsProvider.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Security Settings Provider
|
||||
*
|
||||
* Provides security-related settings including rate limiting and CAPTCHA.
|
||||
*
|
||||
* @package WooNooW
|
||||
*/
|
||||
|
||||
namespace WooNooW\Compat;
|
||||
|
||||
class SecuritySettingsProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Get security settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_settings()
|
||||
{
|
||||
return [
|
||||
// Rate Limiting
|
||||
'enable_checkout_rate_limit' => get_option('woonoow_enable_checkout_rate_limit', 'yes') === 'yes',
|
||||
'rate_limit_orders' => intval(get_option('woonoow_rate_limit_orders', 5)),
|
||||
'rate_limit_minutes' => intval(get_option('woonoow_rate_limit_minutes', 10)),
|
||||
|
||||
// CAPTCHA
|
||||
'captcha_provider' => get_option('woonoow_captcha_provider', 'none'), // none, recaptcha, turnstile
|
||||
'recaptcha_site_key' => get_option('woonoow_recaptcha_site_key', ''),
|
||||
'recaptcha_secret_key' => get_option('woonoow_recaptcha_secret_key', ''),
|
||||
'turnstile_site_key' => get_option('woonoow_turnstile_site_key', ''),
|
||||
'turnstile_secret_key' => get_option('woonoow_turnstile_secret_key', ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public settings (safe to expose to frontend)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_public_settings()
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
return [
|
||||
'captcha_provider' => $settings['captcha_provider'],
|
||||
'recaptcha_site_key' => $settings['recaptcha_site_key'],
|
||||
'turnstile_site_key' => $settings['turnstile_site_key'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update security settings
|
||||
*
|
||||
* @param array $settings
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_settings($settings)
|
||||
{
|
||||
// Rate Limiting
|
||||
if (array_key_exists('enable_checkout_rate_limit', $settings)) {
|
||||
$value = !empty($settings['enable_checkout_rate_limit']) ? 'yes' : 'no';
|
||||
update_option('woonoow_enable_checkout_rate_limit', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['rate_limit_orders'])) {
|
||||
$value = max(1, intval($settings['rate_limit_orders']));
|
||||
update_option('woonoow_rate_limit_orders', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['rate_limit_minutes'])) {
|
||||
$value = max(1, intval($settings['rate_limit_minutes']));
|
||||
update_option('woonoow_rate_limit_minutes', $value);
|
||||
}
|
||||
|
||||
// CAPTCHA Provider
|
||||
if (isset($settings['captcha_provider'])) {
|
||||
$valid_providers = ['none', 'recaptcha', 'turnstile'];
|
||||
$value = in_array($settings['captcha_provider'], $valid_providers)
|
||||
? $settings['captcha_provider']
|
||||
: 'none';
|
||||
update_option('woonoow_captcha_provider', $value);
|
||||
}
|
||||
|
||||
// reCAPTCHA Keys
|
||||
if (isset($settings['recaptcha_site_key'])) {
|
||||
update_option('woonoow_recaptcha_site_key', sanitize_text_field($settings['recaptcha_site_key']));
|
||||
}
|
||||
|
||||
if (isset($settings['recaptcha_secret_key'])) {
|
||||
update_option('woonoow_recaptcha_secret_key', sanitize_text_field($settings['recaptcha_secret_key']));
|
||||
}
|
||||
|
||||
// Turnstile Keys
|
||||
if (isset($settings['turnstile_site_key'])) {
|
||||
update_option('woonoow_turnstile_site_key', sanitize_text_field($settings['turnstile_site_key']));
|
||||
}
|
||||
|
||||
if (isset($settings['turnstile_secret_key'])) {
|
||||
update_option('woonoow_turnstile_secret_key', sanitize_text_field($settings['turnstile_secret_key']));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rate limit is exceeded for an IP
|
||||
*
|
||||
* @param string|null $ip IP address (null = auto-detect)
|
||||
* @return bool True if rate limit exceeded
|
||||
*/
|
||||
public static function is_rate_limited($ip = null)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if (!$settings['enable_checkout_rate_limit']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ip === null) {
|
||||
$ip = self::get_client_ip();
|
||||
}
|
||||
|
||||
$transient_key = 'woonoow_rate_' . md5($ip);
|
||||
$attempts = get_transient($transient_key);
|
||||
|
||||
if ($attempts === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval($attempts) >= $settings['rate_limit_orders'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an order attempt for rate limiting
|
||||
*
|
||||
* @param string|null $ip IP address (null = auto-detect)
|
||||
* @return void
|
||||
*/
|
||||
public static function record_order_attempt($ip = null)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if (!$settings['enable_checkout_rate_limit']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ip === null) {
|
||||
$ip = self::get_client_ip();
|
||||
}
|
||||
|
||||
$transient_key = 'woonoow_rate_' . md5($ip);
|
||||
$attempts = get_transient($transient_key);
|
||||
|
||||
if ($attempts === false) {
|
||||
// First attempt, set with expiration
|
||||
set_transient($transient_key, 1, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS);
|
||||
} else {
|
||||
// Increment attempts (keep same expiration by getting remaining time)
|
||||
$attempts = intval($attempts) + 1;
|
||||
set_transient($transient_key, $attempts, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CAPTCHA token
|
||||
*
|
||||
* @param string $token CAPTCHA token from frontend
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
*/
|
||||
public static function validate_captcha($token)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if ($settings['captcha_provider'] === 'none') {
|
||||
return true; // No CAPTCHA enabled
|
||||
}
|
||||
|
||||
if (empty($token)) {
|
||||
return new \WP_Error('captcha_missing', __('CAPTCHA verification required', 'woonoow'));
|
||||
}
|
||||
|
||||
if ($settings['captcha_provider'] === 'recaptcha') {
|
||||
return self::validate_recaptcha($token, $settings['recaptcha_secret_key']);
|
||||
}
|
||||
|
||||
if ($settings['captcha_provider'] === 'turnstile') {
|
||||
return self::validate_turnstile($token, $settings['turnstile_secret_key']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Google reCAPTCHA v3 token
|
||||
*
|
||||
* @param string $token Token from frontend
|
||||
* @param string $secret_key Secret key
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private static function validate_recaptcha($token, $secret_key)
|
||||
{
|
||||
if (empty($secret_key)) {
|
||||
return new \WP_Error('captcha_config', __('reCAPTCHA not configured', 'woonoow'));
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip(),
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
// reCAPTCHA v3 returns a score (0.0 - 1.0), we accept 0.5 and above
|
||||
$score = $body['score'] ?? 0;
|
||||
if ($score < 0.5) {
|
||||
return new \WP_Error('captcha_score', __('CAPTCHA score too low', 'woonoow'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Cloudflare Turnstile token
|
||||
*
|
||||
* @param string $token Token from frontend
|
||||
* @param string $secret_key Secret key
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private static function validate_turnstile($token, $secret_key)
|
||||
{
|
||||
if (empty($secret_key)) {
|
||||
return new \WP_Error('captcha_config', __('Turnstile not configured', 'woonoow'));
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip(),
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_client_ip()
|
||||
{
|
||||
$headers = [
|
||||
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
$ip = $_SERVER[$header];
|
||||
// Handle comma-separated list (X-Forwarded-For)
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '127.0.0.1';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user