| 1 |
<?php |
| 2 |
|
| 3 |
namespace MasterAddons\Inc\Admin\Settings; |
| 4 |
|
| 5 |
if (!defined('ABSPATH')) { |
| 6 |
exit; // Exit if accessed directly |
| 7 |
} |
| 8 |
|
| 9 |
use MasterAddons\Inc\Admin\Config; |
| 10 |
use MasterAddons\Inc\Admin\REST_API; |
| 11 |
use MasterAddons\Inc\Classes\Helper; |
| 12 |
use MasterAddons\Inc\Classes\Utils; |
| 13 |
use MasterAddons\Inc\Classes\Recommended_Plugins; |
| 14 |
|
| 15 |
/** |
| 16 |
* Centralized Settings Manager for Master Addons |
| 17 |
* |
| 18 |
* Single source of truth for all plugin settings: addons, extensions, third-party |
| 19 |
* plugins, icon libraries, and API keys. Wraps WordPress options (wp_options table) |
| 20 |
* with caching, legacy-key migration, input sanitization, and a fluent query API. |
| 21 |
* |
| 22 |
* Architecture: |
| 23 |
* Settings (singleton) → SettingsProxy (per-group) → SettingsGroup (per-sub-group) |
| 24 |
* |
| 25 |
* Option keys (wp_options): |
| 26 |
* jltma_addons – addon toggle map {addon_key: 0|1} |
| 27 |
* jltma_extensions – extension toggle map {ext_key: 0|1} |
| 28 |
* jltma_plugins – third-party plugin map {plugin_key: 0|1} |
| 29 |
* jltma_icons – icon-library toggle map {icon_key: 0|1} |
| 30 |
* jltma_api – API credentials {service_key: string} |
| 31 |
* |
| 32 |
* Usage examples: |
| 33 |
* |
| 34 |
* 1. Instance chaining (recommended for multiple reads): |
| 35 |
* $s = Settings::instance(); |
| 36 |
* $s->addons->get('accordion'); // single value |
| 37 |
* $s->addons->get(); // full array |
| 38 |
* $s->addons->is_enabled('accordion'); // bool |
| 39 |
* $s->addons->enabled(); // all enabled with Config data |
| 40 |
* $s->addons->group('basic')->all(); // Config items in sub-group |
| 41 |
* $s->addons->group('basic')->enabled(); // enabled items in sub-group |
| 42 |
* $s->addons->group('basic')->counts(); // ['total' => n, 'enabled' => n] |
| 43 |
* $s->addons->group('basic')->enable(); // bulk-enable sub-group |
| 44 |
* |
| 45 |
* 2. Global helper function: |
| 46 |
* jltma_settings()->addons->get('accordion'); |
| 47 |
* jltma_settings()->api->get('google_maps_api_key'); |
| 48 |
* jltma_settings()->get('jltma_white_label_settings'); // any wp_option |
| 49 |
* jltma_settings()->get('jltma_white_label_settings', 'sub_key'); // sub-key access |
| 50 |
* |
| 51 |
* 3. Static facade shorthand (one-liner reads): |
| 52 |
* Settings::addons('accordion'); // = instance()->addons->get('accordion') |
| 53 |
* Settings::api('google_maps_api_key'); // = instance()->api->get(...) |
| 54 |
* |
| 55 |
* 4. Classic static methods (backward-compatible): |
| 56 |
* Settings::get_addons('key'); |
| 57 |
* Settings::is_addon_enabled('key'); |
| 58 |
* Settings::save_addons($array); |
| 59 |
* Settings::get_enabled_addons_by_group('basic'); |
| 60 |
* |
| 61 |
* Security: |
| 62 |
* - Option keys validated against known constants before any DB read/write |
| 63 |
* - Toggle maps sanitized to integer 0|1; API values run through sanitize_text_field() |
| 64 |
* - All array keys sanitized with sanitize_key() |
| 65 |
* |
| 66 |
* @package MasterAddons\Inc\Admin\Settings |
| 67 |
* @since 2.0.0 |
| 68 |
*/ |
| 69 |
class Settings |
| 70 |
{ |
| 71 |
/** |
| 72 |
* Option key constants |
| 73 |
*/ |
| 74 |
const ADDONS_KEY = 'jltma_addons'; |
| 75 |
const EXTENSIONS_KEY = 'jltma_extensions'; |
| 76 |
const PLUGINS_KEY = 'jltma_plugins'; |
| 77 |
const ICONS_KEY = 'jltma_icons'; |
| 78 |
const API_KEY = 'jltma_api'; |
| 79 |
const WHITE_LABEL = 'jltma_white_label_settings'; |
| 80 |
const VERSION_KEY = '_master_addons_version'; |
| 81 |
|
| 82 |
/** |
| 83 |
* Group name → option key map (used by __get and __callStatic) |
| 84 |
*/ |
| 85 |
const PROXY_MAP = [ |
| 86 |
'addons' => self::ADDONS_KEY, |
| 87 |
'extensions' => self::EXTENSIONS_KEY, |
| 88 |
'plugins' => self::PLUGINS_KEY, |
| 89 |
'icons' => self::ICONS_KEY, |
| 90 |
'api' => self::API_KEY, |
| 91 |
]; |
| 92 |
|
| 93 |
/** |
| 94 |
* Legacy option keys for migration |
| 95 |
*/ |
| 96 |
const LEGACY_KEYS = [ |
| 97 |
'maad_el_save_settings' => self::ADDONS_KEY, |
| 98 |
'ma_el_extensions_save_settings' => self::EXTENSIONS_KEY, |
| 99 |
'ma_el_third_party_plugins_save_settings' => self::PLUGINS_KEY, |
| 100 |
'jltma_icons_library_save_settings' => self::ICONS_KEY, |
| 101 |
'jltma_api_save_settings' => self::API_KEY, |
| 102 |
]; |
| 103 |
|
| 104 |
/** |
| 105 |
* Cache for settings |
| 106 |
*/ |
| 107 |
private static $cache = []; |
| 108 |
|
| 109 |
|
| 110 |
public function __construct() |
| 111 |
{ |
| 112 |
add_action('admin_menu', [$this, 'register_admin_menu'], 10); |
| 113 |
add_action('network_admin_menu', [$this, 'register_admin_menu'], 10); |
| 114 |
add_action('current_screen', [$this, 'jltma_set_page_title']); |
| 115 |
add_action('admin_enqueue_scripts', [$this, 'jltma_admin_settings_scripts'], 99); |
| 116 |
add_action('admin_body_class', [$this, 'jltma_admin_body_class']); |
| 117 |
add_action('admin_enqueue_scripts', [$this, 'jltma_global_admin_css']); |
| 118 |
add_action('wp_ajax_jltma_subscribe', [$this, 'handle_subscribe_ajax']); |
| 119 |
} |
| 120 |
|
| 121 |
/** |
| 122 |
* Set page title for MA admin pages before admin-header.php runs. |
| 123 |
* Freemius may override menu registration, leaving global $title unset. |
| 124 |
*/ |
| 125 |
public function jltma_set_page_title($screen) |
| 126 |
{ |
| 127 |
global $title; |
| 128 |
if (!empty($title)) { |
| 129 |
return; |
| 130 |
} |
| 131 |
if (strpos($screen->id, 'master-addons-setup-wizard') !== false) { |
| 132 |
$title = __('Setup Wizard', 'master-addons'); |
| 133 |
} elseif (strpos($screen->id, 'master-addons-settings') !== false) { |
| 134 |
$title = apply_filters('master_addons/white_label/page_title', __('Master Addons for Elementor', 'master-addons')); |
| 135 |
} |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Handle subscribe AJAX request |
| 140 |
* Delegates to the Subscribe notification class |
| 141 |
*/ |
| 142 |
public function handle_subscribe_ajax() |
| 143 |
{ |
| 144 |
$subscribe = new \MasterAddons\Inc\Classes\Notifications\Subscribe(); |
| 145 |
$subscribe->jltma_subscribe(); |
| 146 |
} |
| 147 |
|
| 148 |
|
| 149 |
/** |
| 150 |
* Global admin CSS for menu styling (runs on all admin pages) |
| 151 |
*/ |
| 152 |
public function jltma_global_admin_css() |
| 153 |
{ |
| 154 |
$css = ' |
| 155 |
/* Hide separator below Master Addons menu */ |
| 156 |
#adminmenu #toplevel_page_master-addons-settings + .wp-menu-separator { |
| 157 |
display: none !important; |
| 158 |
} |
| 159 |
#toplevel_page_master-addons-settings { |
| 160 |
margin-bottom: 0 !important; |
| 161 |
padding-bottom: 0 !important; |
| 162 |
} |
| 163 |
|
| 164 |
/* Strip ALL borders from submenu items first */ |
| 165 |
#toplevel_page_master-addons-settings .wp-submenu li { |
| 166 |
border: none !important; |
| 167 |
} |
| 168 |
|
| 169 |
/* Then add back only our separators */ |
| 170 |
#toplevel_page_master-addons-settings .wp-submenu li.jltma-menu-separator { |
| 171 |
border-bottom: 1px solid hsla(0, 0%, 100%, 0.12) !important; |
| 172 |
padding-bottom: 6px; |
| 173 |
margin-bottom: 6px; |
| 174 |
} |
| 175 |
|
| 176 |
|
| 177 |
/* Pricing submenu - full-width green button like Spectra */ |
| 178 |
#toplevel_page_master-addons-settings .wp-submenu li.jltma-menu-pricing a { |
| 179 |
background: #ffa500 !important; |
| 180 |
font-weight: 600 !important; |
| 181 |
padding: 8px 12px !important; |
| 182 |
} |
| 183 |
#toplevel_page_master-addons-settings .wp-submenu li.jltma-menu-pricing a span { |
| 184 |
color: #000 !important; |
| 185 |
} |
| 186 |
#toplevel_page_master-addons-settings .wp-submenu li.jltma-menu-pricing a:hover { |
| 187 |
color: #fff !important; |
| 188 |
background: #16a34a !important; |
| 189 |
} |
| 190 |
#toplevel_page_master-addons-settings .wp-submenu li.jltma-menu-pricing a:hover span { |
| 191 |
color: #fff !important; |
| 192 |
} |
| 193 |
/* ── CPT List Page: Injected Buttons (next to native WP "Add New") ── */ |
| 194 |
.wrap .jltma-cpt-btn { |
| 195 |
display: inline-flex; |
| 196 |
align-items: center; |
| 197 |
gap: 5px; |
| 198 |
padding: 1px 12px; |
| 199 |
font-size: 13px; |
| 200 |
font-weight: 600; |
| 201 |
line-height: 2.15384615; |
| 202 |
min-height: 28px; |
| 203 |
text-decoration: none; |
| 204 |
cursor: pointer; |
| 205 |
white-space: nowrap; |
| 206 |
border-radius: 3px; |
| 207 |
box-sizing: content-box; |
| 208 |
vertical-align: baseline; |
| 209 |
margin-left: 8px; |
| 210 |
} |
| 211 |
.wrap .jltma-cpt-btn svg { |
| 212 |
flex-shrink: 0; |
| 213 |
} |
| 214 |
.wrap .jltma-cpt-btn-secondary { |
| 215 |
background: linear-gradient(135deg, rgb(153, 41, 234) 0%, rgb(88, 8, 251) 100%); |
| 216 |
color: #fff; |
| 217 |
border: none; |
| 218 |
transition: opacity 0.15s ease; |
| 219 |
} |
| 220 |
.wrap .jltma-cpt-btn-secondary:hover, |
| 221 |
.wrap .jltma-cpt-btn-secondary:focus { |
| 222 |
opacity: 0.88; |
| 223 |
color: #fff; |
| 224 |
} |
| 225 |
.wrap .jltma-cpt-btn-youtube { |
| 226 |
background: linear-gradient(135deg, #fff5f5 0%, #ffe0e0 100%); |
| 227 |
color: #cc0000; |
| 228 |
border: 1px solid #fecaca; |
| 229 |
transition: border-color 0.15s ease, background 0.15s ease; |
| 230 |
} |
| 231 |
.wrap .jltma-cpt-btn-youtube:hover, |
| 232 |
.wrap .jltma-cpt-btn-youtube:focus { |
| 233 |
background: linear-gradient(135deg, #ffe0e0 0%, #fecaca 100%); |
| 234 |
border-color: #f87171; |
| 235 |
color: #b91c1c; |
| 236 |
} |
| 237 |
.wrap .jltma-cpt-btn-youtube svg { |
| 238 |
fill: #ff0000; |
| 239 |
} |
| 240 |
'; |
| 241 |
|
| 242 |
// JS to reorder submenu items and add separator/pricing classes |
| 243 |
$js = ' |
| 244 |
document.addEventListener("DOMContentLoaded", function() { |
| 245 |
var menu = document.getElementById("toplevel_page_master-addons-settings"); |
| 246 |
if (!menu) return; |
| 247 |
var submenu = menu.querySelector(".wp-submenu"); |
| 248 |
if (!submenu) return; |
| 249 |
|
| 250 |
// Reorder: Move "Template Kits" right after "Template Library" |
| 251 |
var items = submenu.querySelectorAll("li a"); |
| 252 |
var libraryItem = null, kitsItem = null, wizardItem = null, recommendedItem = null, widgetsLibItem = null; |
| 253 |
items.forEach(function(a) { |
| 254 |
var href = a.getAttribute("href") || ""; |
| 255 |
if (href.indexOf("jltma-widgets-library") !== -1) widgetsLibItem = a.parentElement; |
| 256 |
if (href.indexOf("jltma-template-library") !== -1) libraryItem = a.parentElement; |
| 257 |
if (href.indexOf("jltma-template-kits") !== -1) kitsItem = a.parentElement; |
| 258 |
if (href.indexOf("master-addons-setup-wizard") !== -1) wizardItem = a.parentElement; |
| 259 |
if (href.indexOf("master-addons-recommended") !== -1) recommendedItem = a.parentElement; |
| 260 |
}); |
| 261 |
// Place Template Kits right after Template Library |
| 262 |
if (libraryItem && kitsItem && libraryItem.nextElementSibling !== kitsItem) { |
| 263 |
submenu.insertBefore(kitsItem, libraryItem.nextElementSibling); |
| 264 |
} |
| 265 |
// Place Widgets Library immediately before Template Library |
| 266 |
if (libraryItem && widgetsLibItem && libraryItem.previousElementSibling !== widgetsLibItem) { |
| 267 |
submenu.insertBefore(widgetsLibItem, libraryItem); |
| 268 |
} |
| 269 |
// Move "Setup Wizard" just before "Recommended" |
| 270 |
if (wizardItem && recommendedItem) { |
| 271 |
submenu.insertBefore(wizardItem, recommendedItem); |
| 272 |
} |
| 273 |
|
| 274 |
// Separators and pricing |
| 275 |
var separatorAfter = [ |
| 276 |
"page=master-addons-settings", |
| 277 |
"jltma-template-kits", |
| 278 |
"post_type=jltma_widget" |
| 279 |
]; |
| 280 |
submenu.querySelectorAll("li a").forEach(function(a) { |
| 281 |
var href = a.getAttribute("href") || ""; |
| 282 |
for (var i = 0; i < separatorAfter.length; i++) { |
| 283 |
var pos = href.indexOf(separatorAfter[i]); |
| 284 |
if (pos === -1) { continue; } |
| 285 |
// Require a param boundary after the match so |
| 286 |
// "page=master-addons-settings" does NOT also match |
| 287 |
// "page=master-addons-settings-account" (the Account item). |
| 288 |
var nextChar = href.charAt(pos + separatorAfter[i].length); |
| 289 |
if (nextChar === "" || nextChar === "&" || nextChar === "#" || nextChar === "\"") { |
| 290 |
a.parentElement.classList.add("jltma-menu-separator"); |
| 291 |
break; |
| 292 |
} |
| 293 |
} |
| 294 |
var text = (a.textContent || "").trim().toLowerCase(); |
| 295 |
if (href.indexOf("pricing") !== -1 || text.indexOf("pricing") !== -1 || text.indexOf("upgrade") !== -1) { |
| 296 |
a.parentElement.classList.add("jltma-menu-pricing"); |
| 297 |
} |
| 298 |
}); |
| 299 |
}); |
| 300 |
'; |
| 301 |
|
| 302 |
// Load the menu styling and ordering through the core enqueue APIs |
| 303 |
// (inline-only handles) instead of hardcoded <style>/<script> tags. |
| 304 |
wp_register_style('jltma-admin-menu', false, array(), JLTMA_VER); |
| 305 |
wp_enqueue_style('jltma-admin-menu'); |
| 306 |
wp_add_inline_style('jltma-admin-menu', $css); |
| 307 |
|
| 308 |
wp_register_script('jltma-admin-menu', false, array(), JLTMA_VER, true); |
| 309 |
wp_enqueue_script('jltma-admin-menu'); |
| 310 |
wp_add_inline_script('jltma-admin-menu', $js); |
| 311 |
} |
| 312 |
|
| 313 |
/** |
| 314 |
* Admin Body Class |
| 315 |
*/ |
| 316 |
public function jltma_admin_body_class($class) |
| 317 |
{ |
| 318 |
$bodyclass = ''; |
| 319 |
$bodyclass .= ' jltma-admin '; |
| 320 |
return $class . $bodyclass; |
| 321 |
} |
| 322 |
|
| 323 |
|
| 324 |
public function jltma_admin_settings_scripts() { |
| 325 |
$screen = get_current_screen(); |
| 326 |
|
| 327 |
$jltma_menu_label = __('Master Addons', 'master-addons'); |
| 328 |
$menu_label = apply_filters('master_addons/white_label/menu_label', $jltma_menu_label); |
| 329 |
|
| 330 |
// Check if we're on any Master Addons admin page |
| 331 |
$is_master_addons_page = ( |
| 332 |
$screen->id == 'toplevel_page_master-addons-settings' || |
| 333 |
$screen->id == 'toplevel_page_master-addons-settings-network' || |
| 334 |
// strpos($screen->id, 'master-addons_page_') === 0 || |
| 335 |
$screen->id === strtolower(preg_replace('/\s+/', '-', trim($menu_label))) . '_page_master-addons-settings' |
| 336 |
// || (isset($screen->parent_base) && $screen->parent_base === 'master-addons-settings') |
| 337 |
); |
| 338 |
|
| 339 |
// Setup Wizard page |
| 340 |
$is_setup_wizard_page = ( |
| 341 |
strpos($screen->id, 'master-addons-setup-wizard') !== false |
| 342 |
); |
| 343 |
|
| 344 |
// Load Scripts only Master Addons Admin Page |
| 345 |
if ($is_master_addons_page && empty( $is_setup_wizard_page )) { |
| 346 |
|
| 347 |
// Hide all admin notices on settings page |
| 348 |
remove_all_actions('admin_notices'); |
| 349 |
remove_all_actions('all_admin_notices'); |
| 350 |
remove_all_actions('network_admin_notices'); |
| 351 |
remove_all_actions('user_admin_notices'); |
| 352 |
remove_action('admin_notices', 'update_nag', 3); |
| 353 |
remove_action('admin_notices', 'maintenance_nag', 10); |
| 354 |
|
| 355 |
// Hide any remaining admin notices on our settings screen. Attached to the |
| 356 |
// settings stylesheet (enqueued below) instead of a hardcoded <style> tag. |
| 357 |
$jltma_hide_notices_css = ' |
| 358 |
.notice, .error, .updated, .update-nag, .admin-notice, |
| 359 |
.jltma-plugin-update-notice, .fs-notice, .fs-slug-master-addons, |
| 360 |
#wpbody-content > .notice, #wpbody-content > .error, #wpbody-content > .updated, |
| 361 |
.wrap > .notice, .wrap > .error, .wrap > .updated { |
| 362 |
display: none !important; |
| 363 |
} |
| 364 |
#wpcontent { |
| 365 |
padding-left: 0 !important; |
| 366 |
} |
| 367 |
#wpfooter { |
| 368 |
display: none !important; |
| 369 |
}'; |
| 370 |
|
| 371 |
// Dequeue Spectra (UAG) zipwp-images style to prevent CSS conflicts on settings page |
| 372 |
add_action('admin_enqueue_scripts', function () { |
| 373 |
wp_dequeue_style('zipwp-images-style'); |
| 374 |
}, 9999); |
| 375 |
|
| 376 |
if (!did_action('wp_enqueue_media')) { |
| 377 |
wp_enqueue_media(); |
| 378 |
} |
| 379 |
|
| 380 |
// Elementor icons for addon card icons — use Elementor's copy if available, otherwise local fallback |
| 381 |
if (wp_style_is('elementor-icons', 'registered')) { |
| 382 |
wp_enqueue_style('elementor-icons'); |
| 383 |
} else { |
| 384 |
wp_enqueue_style('jltma-elementor-icons'); |
| 385 |
} |
| 386 |
|
| 387 |
// Only load React app on the React settings page to avoid Lucide icons conflicting with native Image constructor |
| 388 |
wp_enqueue_style('jltma-admin-settings'); |
| 389 |
wp_enqueue_script('jltma-admin-settings'); |
| 390 |
|
| 391 |
wp_add_inline_style('jltma-admin-settings', $jltma_hide_notices_css); |
| 392 |
|
| 393 |
// White Label logo & Hidden Nav Menus |
| 394 |
$white_label_settings = jltma_settings()->get('jltma_white_label_settings'); |
| 395 |
$white_label_settings = is_array($white_label_settings) ? $white_label_settings : []; |
| 396 |
$white_label_logo = ''; |
| 397 |
|
| 398 |
// White Label Logo |
| 399 |
if( !empty(Utils::check_options($white_label_settings['jltma_wl_plugin_logo'] ?? '') ) ) { |
| 400 |
$white_label_logo = wp_get_attachment_image_src($white_label_settings['jltma_wl_plugin_logo'])[0]; |
| 401 |
} |
| 402 |
|
| 403 |
// Hidden Nav Menus — keys must match nav_menus.ts ids |
| 404 |
$hidden_menus = []; |
| 405 |
$tab_keys = [ |
| 406 |
'welcome' => 'jltma_wl_plugin_tab_welcome', |
| 407 |
'addons' => 'jltma_wl_plugin_tab_addons', |
| 408 |
'extensions' => 'jltma_wl_plugin_tab_extensions', |
| 409 |
'tools' => 'jltma_wl_plugin_tab_tools', |
| 410 |
'free_vs_pro' => 'jltma_wl_plugin_tab_free_vs_pro', |
| 411 |
'white_label' => 'jltma_wl_plugin_tab_white_label', |
| 412 |
'template_kits' => 'jltma_wl_plugin_tab_template_kits', |
| 413 |
]; |
| 414 |
foreach ($tab_keys as $menu_id => $db_key) { |
| 415 |
$hidden_menus[$menu_id] = !empty($white_label_settings[$db_key]) ? true : false; |
| 416 |
} |
| 417 |
|
| 418 |
$current_user = wp_get_current_user(); |
| 419 |
|
| 420 |
$localize_data = [ |
| 421 |
'pluginSlug' => 'master-addons', |
| 422 |
'restUrl' => rest_url('master-addons/v1'), |
| 423 |
'nonce' => wp_create_nonce('wp_rest'), |
| 424 |
'adminUrl' => admin_url(), |
| 425 |
'assetsUrl' => JLTMA_ASSETS, |
| 426 |
'logo' => array( |
| 427 |
'light' => $white_label_logo ? $white_label_logo : JLTMA_ASSETS . 'images/full-logo.svg', |
| 428 |
'dark' => JLTMA_ASSETS . 'images/full-logo.png', |
| 429 |
), |
| 430 |
'darkMode' => 'light', |
| 431 |
'data' => Config::get_config(), |
| 432 |
'is_premium' => ma_el_fs()->can_use_premium_code__premium_only(), |
| 433 |
'is_developer' => ma_el_fs()->is_plan__premium_only('developer'), |
| 434 |
'hidden_menus' => $hidden_menus, |
| 435 |
'is_setup_complete' => REST_API::is_setup_complete(), |
| 436 |
'user_email' => $current_user->user_email, |
| 437 |
'user_name' => $current_user->display_name, |
| 438 |
'subscribe_nonce' => wp_create_nonce('jltma_subscribe_nonce'), |
| 439 |
'version' => JLTMA_VER, |
| 440 |
]; |
| 441 |
|
| 442 |
wp_localize_script('jltma-admin-settings', 'JLTMA_SETTINGS', $localize_data); |
| 443 |
} |
| 444 |
|
| 445 |
if ($is_setup_wizard_page) { |
| 446 |
$this->jltma_setup_wizard_scrips(); |
| 447 |
} |
| 448 |
|
| 449 |
// Recommended Plugins page assets |
| 450 |
$is_recommended_page = ( |
| 451 |
strpos($screen->id, 'master-addons-recommended-plugins') !== false |
| 452 |
); |
| 453 |
if ($is_recommended_page) { |
| 454 |
wp_enqueue_style('jltma-recommended-plugins'); |
| 455 |
wp_enqueue_script('jltma-recommended-plugins'); |
| 456 |
} |
| 457 |
|
| 458 |
// ADMIN SDK Localize |
| 459 |
wp_enqueue_style('jltma-admin-sdk'); |
| 460 |
wp_enqueue_script('jltma-admin-sdk'); |
| 461 |
|
| 462 |
wp_localize_script( |
| 463 |
'jltma-admin-sdk', |
| 464 |
'JLTMACORE', |
| 465 |
array( |
| 466 |
'admin_ajax' => admin_url('admin-ajax.php'), |
| 467 |
'recommended_nonce' => wp_create_nonce('jltma_recommended_nonce'), |
| 468 |
'is_premium' => Helper::jltma_premium(), |
| 469 |
) |
| 470 |
); |
| 471 |
} |
| 472 |
|
| 473 |
/** |
| 474 |
* Enqueue scripts and styles for the Setup Wizard page, and hide admin UI elements for a full-screen experience |
| 475 |
* Note: This is a hidden page only accessed via direct URL, so we can safely hide all admin notices and UI elements without affecting other pages |
| 476 |
*/ |
| 477 |
public function jltma_setup_wizard_scrips() { |
| 478 |
// Hide admin notices, admin bar, and sidebar for full-screen experience |
| 479 |
remove_all_actions('admin_notices'); |
| 480 |
remove_all_actions('all_admin_notices'); |
| 481 |
remove_all_actions('network_admin_notices'); |
| 482 |
remove_all_actions('user_admin_notices'); |
| 483 |
|
| 484 |
// Hide the footer for the full-screen wizard (attached to the wizard |
| 485 |
// stylesheet below instead of a hardcoded <style> tag). |
| 486 |
$jltma_wizard_css = '#wpfooter { display: none !important; }'; |
| 487 |
|
| 488 |
// Elementor icons for addon card icons |
| 489 |
if (wp_style_is('elementor-icons', 'registered')) { |
| 490 |
wp_enqueue_style('elementor-icons'); |
| 491 |
} else { |
| 492 |
wp_enqueue_style('jltma-elementor-icons'); |
| 493 |
} |
| 494 |
|
| 495 |
wp_enqueue_style('jltma-setup-wizard'); |
| 496 |
wp_enqueue_script('jltma-setup-wizard'); |
| 497 |
|
| 498 |
wp_add_inline_style('jltma-setup-wizard', $jltma_wizard_css); |
| 499 |
|
| 500 |
// Build recommended plugins data with pre-computed install status. |
| 501 |
$recommended_instance = Recommended_Plugins::get_instance(); |
| 502 |
$raw_plugins = $recommended_instance->plugins_list(); |
| 503 |
$recommended_plugins = []; |
| 504 |
|
| 505 |
if ( ! function_exists( 'install_plugin_install_status' ) ) { |
| 506 |
require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; |
| 507 |
} |
| 508 |
if ( ! function_exists( 'is_plugin_active' ) ) { |
| 509 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 510 |
} |
| 511 |
|
| 512 |
foreach ( $raw_plugins as $slug => $plugin ) { |
| 513 |
$plugin_api = (object) $plugin; |
| 514 |
if ( ! isset( $plugin_api->version ) ) { |
| 515 |
$plugin_api->version = ''; |
| 516 |
} |
| 517 |
$install_status = \install_plugin_install_status( $plugin_api ); |
| 518 |
|
| 519 |
// Map install_plugin_install_status() result to simple status. |
| 520 |
if ( 'install' === $install_status['status'] ) { |
| 521 |
$status = 'not_installed'; |
| 522 |
$plugin_file = ''; |
| 523 |
} elseif ( ! empty( $install_status['file'] ) && is_plugin_active( $install_status['file'] ) ) { |
| 524 |
$status = 'active'; |
| 525 |
$plugin_file = $install_status['file']; |
| 526 |
} else { |
| 527 |
$status = 'installed'; |
| 528 |
$plugin_file = $install_status['file'] ?? ''; |
| 529 |
} |
| 530 |
|
| 531 |
$recommended_plugins[] = [ |
| 532 |
'slug' => $plugin['slug'], |
| 533 |
'name' => $plugin['name'], |
| 534 |
'icon' => $plugin['icon'], |
| 535 |
'download_link' => $plugin['download_link'], |
| 536 |
'status' => $status, |
| 537 |
'plugin_file' => $plugin_file, |
| 538 |
]; |
| 539 |
} |
| 540 |
|
| 541 |
$localize_data = [ |
| 542 |
'pluginSlug' => 'master-addons', |
| 543 |
'restUrl' => rest_url('master-addons/v1'), |
| 544 |
'nonce' => wp_create_nonce('wp_rest'), |
| 545 |
'adminUrl' => admin_url(), |
| 546 |
'ajaxUrl' => admin_url('admin-ajax.php'), |
| 547 |
'assetsUrl' => JLTMA_ASSETS, |
| 548 |
'recommended_nonce' => wp_create_nonce('jltma_recommended_nonce'), |
| 549 |
'recommended_plugins' => $recommended_plugins, |
| 550 |
'logo' => array( |
| 551 |
'light' => JLTMA_ASSETS . 'images/full-logo.svg', |
| 552 |
'dark' => JLTMA_ASSETS . 'images/full-logo.png', |
| 553 |
), |
| 554 |
'darkMode' => 'light', |
| 555 |
'data' => Config::get_config(), |
| 556 |
'is_premium' => ma_el_fs()->can_use_premium_code__premium_only(), |
| 557 |
'is_developer' => ma_el_fs()->is_plan__premium_only('developer'), |
| 558 |
'hidden_menus' => [], |
| 559 |
'step_details' => REST_API::get_instance()->get_setup_status()->data |
| 560 |
]; |
| 561 |
|
| 562 |
wp_localize_script('jltma-setup-wizard', 'JLTMA_SETUP_WIZARD', $localize_data); |
| 563 |
} |
| 564 |
|
| 565 |
/** |
| 566 |
* Singleton instance |
| 567 |
* |
| 568 |
* @var self|null |
| 569 |
*/ |
| 570 |
private static $instance = null; |
| 571 |
|
| 572 |
/** |
| 573 |
* Cached SettingsProxy instances (one per group) |
| 574 |
* |
| 575 |
* @var SettingsProxy[] |
| 576 |
*/ |
| 577 |
private $proxies = []; |
| 578 |
|
| 579 |
|
| 580 |
/** |
| 581 |
* Register admin menu page |
| 582 |
*/ |
| 583 |
public function register_admin_menu() |
| 584 |
{ |
| 585 |
$image_dir = defined('JLTMA_IMAGE_DIR') ? JLTMA_IMAGE_DIR : (defined('JLTMA_PRO_IMAGE_DIR') ? JLTMA_PRO_IMAGE_DIR : ''); |
| 586 |
$logo = $image_dir . 'red-logo.svg'; |
| 587 |
$page_title = __('Master Addons for Elementor', 'master-addons'); |
| 588 |
$menu_label = __('Master Addons', 'master-addons'); |
| 589 |
|
| 590 |
$logo = apply_filters('master_addons/white_label/menu_logo', $logo); |
| 591 |
$page_title = apply_filters('master_addons/white_label/page_title', $page_title); |
| 592 |
$menu_label = apply_filters('master_addons/white_label/menu_label', $menu_label); |
| 593 |
|
| 594 |
// Update badge |
| 595 |
$update_plugins = get_site_transient('update_plugins'); |
| 596 |
$has_update = (defined('JLTMA_BASE') && !empty($update_plugins->response[JLTMA_BASE])) |
| 597 |
|| (defined('JLTMA_PRO_BASE') && !empty($update_plugins->response[JLTMA_PRO_BASE])); |
| 598 |
|
| 599 |
if ($has_update) { |
| 600 |
$menu_label = sprintf('%s <span class="jltma-menu-notice">1</span>', $menu_label); |
| 601 |
} |
| 602 |
|
| 603 |
add_menu_page( |
| 604 |
$page_title, |
| 605 |
$menu_label, |
| 606 |
'manage_options', |
| 607 |
'master-addons-settings', |
| 608 |
[$this, 'render_settings_page'], |
| 609 |
$logo, |
| 610 |
57 |
| 611 |
); |
| 612 |
|
| 613 |
add_submenu_page( |
| 614 |
'master-addons-settings', |
| 615 |
$page_title, |
| 616 |
__('Settings', 'master-addons'), |
| 617 |
'manage_options', |
| 618 |
'master-addons-settings', |
| 619 |
[$this, 'render_settings_page'] |
| 620 |
); |
| 621 |
|
| 622 |
// Setup Wizard - registered with empty parent for Freemius-independent access, |
| 623 |
// plus a visible submenu link under Master Addons until completed |
| 624 |
if ( ! REST_API::is_setup_complete() ) { |
| 625 |
// Hidden page registration (always accessible regardless of Freemius state) |
| 626 |
add_submenu_page( |
| 627 |
'', |
| 628 |
__('Setup Wizard', 'master-addons'), |
| 629 |
__('Setup Wizard', 'master-addons'), |
| 630 |
'manage_options', |
| 631 |
'master-addons-setup-wizard', |
| 632 |
[$this, 'jltma_setup_wizard_page_content'] |
| 633 |
); |
| 634 |
|
| 635 |
// Visible menu link under Master Addons |
| 636 |
global $submenu; |
| 637 |
$submenu['master-addons-settings'][] = array( |
| 638 |
__('Setup Wizard', 'master-addons'), |
| 639 |
'manage_options', |
| 640 |
'admin.php?page=master-addons-setup-wizard', |
| 641 |
); |
| 642 |
} |
| 643 |
} |
| 644 |
|
| 645 |
/** |
| 646 |
* Render the settings page (loads welcome.php template) |
| 647 |
*/ |
| 648 |
public function render_settings_page() |
| 649 |
{ |
| 650 |
echo '<div id="jltma-admin-settings-root"></div>'; |
| 651 |
} |
| 652 |
|
| 653 |
/** |
| 654 |
* Setup Wizard page content |
| 655 |
*/ |
| 656 |
public function jltma_setup_wizard_page_content() |
| 657 |
{ |
| 658 |
echo '<div id="jltma-setup-wizard-root"></div>'; |
| 659 |
} |
| 660 |
|
| 661 |
/** |
| 662 |
* Get singleton instance |
| 663 |
* |
| 664 |
* @return self |
| 665 |
*/ |
| 666 |
public static function instance() |
| 667 |
{ |
| 668 |
if (self::$instance === null) { |
| 669 |
self::$instance = new self(); |
| 670 |
} |
| 671 |
|
| 672 |
return self::$instance; |
| 673 |
} |
| 674 |
|
| 675 |
/** |
| 676 |
* Generic option getter for any wp_options key |
| 677 |
* |
| 678 |
* Reads a raw WordPress option with in-memory caching. |
| 679 |
* Works with any option key — not limited to PROXY_MAP groups. |
| 680 |
* |
| 681 |
* Usage: |
| 682 |
* jltma_settings()->get('jltma_white_label_settings'); |
| 683 |
* jltma_settings()->get('jltma_white_label_settings', 'jltma_wl_plugin_logo'); |
| 684 |
* jltma_settings()->get('jltma_white_label_settings', 'missing_key', 'fallback'); |
| 685 |
* |
| 686 |
* @param string $option_key WordPress option name |
| 687 |
* @param string|null $sub_key Optional sub-key within the option array |
| 688 |
* @param mixed $default Default value when key is missing |
| 689 |
* @return mixed |
| 690 |
*/ |
| 691 |
public function get($option_key, $sub_key = null, $default = null) |
| 692 |
{ |
| 693 |
if (!isset(self::$cache[$option_key])) { |
| 694 |
self::$cache[$option_key] = get_option($option_key, []); |
| 695 |
} |
| 696 |
|
| 697 |
$settings = self::$cache[$option_key]; |
| 698 |
|
| 699 |
if ($sub_key === null) { |
| 700 |
return $settings ?: $default; |
| 701 |
} |
| 702 |
|
| 703 |
return isset($settings[$sub_key]) ? $settings[$sub_key] : $default; |
| 704 |
} |
| 705 |
|
| 706 |
/** |
| 707 |
* Magic property access → returns cached SettingsProxy |
| 708 |
* |
| 709 |
* Enables: Settings::instance()->addons->get('key') |
| 710 |
* |
| 711 |
* @param string $name Group name (addons, extensions, plugins, icons, api) |
| 712 |
* @return SettingsProxy |
| 713 |
* @throws \InvalidArgumentException If group name is invalid |
| 714 |
*/ |
| 715 |
public function __get($name) |
| 716 |
{ |
| 717 |
if (!isset(self::PROXY_MAP[$name])) { |
| 718 |
throw new \InvalidArgumentException( |
| 719 |
esc_html(sprintf('Unknown settings group "%s". Valid groups: %s', $name, implode(', ', array_keys(self::PROXY_MAP)))) |
| 720 |
); |
| 721 |
} |
| 722 |
|
| 723 |
if (!isset($this->proxies[$name])) { |
| 724 |
$this->proxies[$name] = new SettingsProxy($name, self::PROXY_MAP[$name]); |
| 725 |
} |
| 726 |
|
| 727 |
return $this->proxies[$name]; |
| 728 |
} |
| 729 |
|
| 730 |
/** |
| 731 |
* Static facade shorthand |
| 732 |
* |
| 733 |
* Enables: Settings::addons('key') as shorthand for Settings::instance()->addons->get('key') |
| 734 |
* |
| 735 |
* @param string $name Group name |
| 736 |
* @param array $arguments [0] = key (optional), [1] = default (optional) |
| 737 |
* @return mixed |
| 738 |
*/ |
| 739 |
public static function __callStatic($name, $arguments) |
| 740 |
{ |
| 741 |
if (isset(self::PROXY_MAP[$name])) { |
| 742 |
$proxy = self::instance()->$name; |
| 743 |
$key = $arguments[0] ?? null; |
| 744 |
$default = $arguments[1] ?? null; |
| 745 |
|
| 746 |
return $proxy->get($key, $default); |
| 747 |
} |
| 748 |
|
| 749 |
throw new \BadMethodCallException( |
| 750 |
esc_html(sprintf('Call to undefined method %s::%s()', static::class, $name)) |
| 751 |
); |
| 752 |
} |
| 753 |
|
| 754 |
// ------------------------------------------------------------------------- |
| 755 |
// Existing static API (unchanged signatures) |
| 756 |
// ------------------------------------------------------------------------- |
| 757 |
|
| 758 |
/** |
| 759 |
* Get addons settings (enabled/disabled state) |
| 760 |
* |
| 761 |
* @param string|null $key Specific addon key or null for all |
| 762 |
* @param mixed $default Default value |
| 763 |
* @return mixed |
| 764 |
*/ |
| 765 |
public static function get_addons($key = null, $default = null) |
| 766 |
{ |
| 767 |
return self::get_option(self::ADDONS_KEY, $key, $default); |
| 768 |
} |
| 769 |
|
| 770 |
/** |
| 771 |
* Save addons settings |
| 772 |
* |
| 773 |
* @param array $settings Settings array |
| 774 |
* @return bool |
| 775 |
*/ |
| 776 |
public static function save_addons($settings) |
| 777 |
{ |
| 778 |
return self::save_option(self::ADDONS_KEY, $settings); |
| 779 |
} |
| 780 |
|
| 781 |
/** |
| 782 |
* Get extensions settings (enabled/disabled state) |
| 783 |
* |
| 784 |
* @param string|null $key Specific extension key or null for all |
| 785 |
* @param mixed $default Default value |
| 786 |
* @return mixed |
| 787 |
*/ |
| 788 |
public static function get_extensions($key = null, $default = null) |
| 789 |
{ |
| 790 |
return self::get_option(self::EXTENSIONS_KEY, $key, $default); |
| 791 |
} |
| 792 |
|
| 793 |
/** |
| 794 |
* Save extensions settings |
| 795 |
* |
| 796 |
* @param array $settings Settings array |
| 797 |
* @return bool |
| 798 |
*/ |
| 799 |
public static function save_extensions($settings) |
| 800 |
{ |
| 801 |
return self::save_option(self::EXTENSIONS_KEY, $settings); |
| 802 |
} |
| 803 |
|
| 804 |
/** |
| 805 |
* Get third-party plugins settings |
| 806 |
* |
| 807 |
* @param string|null $key Specific plugin key or null for all |
| 808 |
* @param mixed $default Default value |
| 809 |
* @return mixed |
| 810 |
*/ |
| 811 |
public static function get_plugins($key = null, $default = null) |
| 812 |
{ |
| 813 |
return self::get_option(self::PLUGINS_KEY, $key, $default); |
| 814 |
} |
| 815 |
|
| 816 |
/** |
| 817 |
* Save third-party plugins settings |
| 818 |
* |
| 819 |
* @param array $settings Settings array |
| 820 |
* @return bool |
| 821 |
*/ |
| 822 |
public static function save_plugins($settings) |
| 823 |
{ |
| 824 |
return self::save_option(self::PLUGINS_KEY, $settings); |
| 825 |
} |
| 826 |
|
| 827 |
/** |
| 828 |
* Get icons library settings |
| 829 |
* |
| 830 |
* @param string|null $key Specific icon key or null for all |
| 831 |
* @param mixed $default Default value |
| 832 |
* @return mixed |
| 833 |
*/ |
| 834 |
public static function get_icons($key = null, $default = null) |
| 835 |
{ |
| 836 |
return self::get_option(self::ICONS_KEY, $key, $default); |
| 837 |
} |
| 838 |
|
| 839 |
/** |
| 840 |
* Save icons library settings |
| 841 |
* |
| 842 |
* @param array $settings Settings array |
| 843 |
* @return bool |
| 844 |
*/ |
| 845 |
public static function save_icons($settings) |
| 846 |
{ |
| 847 |
return self::save_option(self::ICONS_KEY, $settings); |
| 848 |
} |
| 849 |
|
| 850 |
/** |
| 851 |
* Get API settings |
| 852 |
* |
| 853 |
* @param string|null $key Specific API key or null for all |
| 854 |
* @param mixed $default Default value |
| 855 |
* @return mixed |
| 856 |
*/ |
| 857 |
public static function get_api($key = null, $default = null) |
| 858 |
{ |
| 859 |
return self::get_option(self::API_KEY, $key, $default); |
| 860 |
} |
| 861 |
|
| 862 |
/** |
| 863 |
* Save API settings |
| 864 |
* |
| 865 |
* @param array $settings Settings array |
| 866 |
* @return bool |
| 867 |
*/ |
| 868 |
public static function save_api($settings) |
| 869 |
{ |
| 870 |
return self::save_option(self::API_KEY, $settings); |
| 871 |
} |
| 872 |
|
| 873 |
/** |
| 874 |
* Check if an addon is enabled |
| 875 |
* |
| 876 |
* @param string $addon_key |
| 877 |
* @return bool |
| 878 |
*/ |
| 879 |
public static function is_addon_enabled($addon_key) |
| 880 |
{ |
| 881 |
return (bool) self::get_addons($addon_key, true); |
| 882 |
} |
| 883 |
|
| 884 |
/** |
| 885 |
* Check if an extension is enabled |
| 886 |
* |
| 887 |
* @param string $extension_key |
| 888 |
* @return bool |
| 889 |
*/ |
| 890 |
public static function is_extension_enabled($extension_key) |
| 891 |
{ |
| 892 |
return (bool) self::get_extensions($extension_key, true); |
| 893 |
} |
| 894 |
|
| 895 |
/** |
| 896 |
* Check if a plugin integration is enabled |
| 897 |
* |
| 898 |
* @param string $plugin_key |
| 899 |
* @return bool |
| 900 |
*/ |
| 901 |
public static function is_plugin_enabled($plugin_key) |
| 902 |
{ |
| 903 |
return (bool) self::get_plugins($plugin_key, true); |
| 904 |
} |
| 905 |
|
| 906 |
/** |
| 907 |
* Check if an icon library is enabled |
| 908 |
* |
| 909 |
* @param string $icon_key |
| 910 |
* @return bool |
| 911 |
*/ |
| 912 |
public static function is_icon_enabled($icon_key) |
| 913 |
{ |
| 914 |
return (bool) self::get_icons($icon_key, true); |
| 915 |
} |
| 916 |
|
| 917 |
/** |
| 918 |
* Get enabled addons with full config data |
| 919 |
* Merges enabled state with addon definitions from Config |
| 920 |
* |
| 921 |
* @return array |
| 922 |
*/ |
| 923 |
public static function get_enabled_addons() |
| 924 |
{ |
| 925 |
$enabled_settings = self::get_addons() ?: []; |
| 926 |
$all_addons = Config::get_addons(); |
| 927 |
$enabled = []; |
| 928 |
|
| 929 |
foreach ($all_addons as $key => $addon) { |
| 930 |
if (!empty($enabled_settings[$key])) { |
| 931 |
$enabled[$key] = $addon; |
| 932 |
} |
| 933 |
} |
| 934 |
|
| 935 |
return $enabled; |
| 936 |
} |
| 937 |
|
| 938 |
/** |
| 939 |
* Get enabled addons by group |
| 940 |
* |
| 941 |
* @param string $group Group key |
| 942 |
* @return array |
| 943 |
*/ |
| 944 |
public static function get_enabled_addons_by_group($group) |
| 945 |
{ |
| 946 |
$enabled_settings = self::get_addons() ?: []; |
| 947 |
$group_addons = Config::get_addons_by_group($group); |
| 948 |
$enabled = []; |
| 949 |
|
| 950 |
foreach ($group_addons as $key => $addon) { |
| 951 |
if (!empty($enabled_settings[$key])) { |
| 952 |
$enabled[$key] = $addon; |
| 953 |
} |
| 954 |
} |
| 955 |
|
| 956 |
return $enabled; |
| 957 |
} |
| 958 |
|
| 959 |
/** |
| 960 |
* Get enabled addons by group and subcategory |
| 961 |
* |
| 962 |
* @param string $group Group key |
| 963 |
* @param string $subcategory Subcategory key |
| 964 |
* @return array |
| 965 |
*/ |
| 966 |
public static function get_enabled_addons_by_subcategory($group, $subcategory) |
| 967 |
{ |
| 968 |
$enabled_settings = self::get_addons() ?: []; |
| 969 |
$subcategory_addons = Config::get_addons_by_subcategory($group, $subcategory); |
| 970 |
$enabled = []; |
| 971 |
|
| 972 |
foreach ($subcategory_addons as $key => $addon) { |
| 973 |
if (!empty($enabled_settings[$key])) { |
| 974 |
$enabled[$key] = $addon; |
| 975 |
} |
| 976 |
} |
| 977 |
|
| 978 |
return $enabled; |
| 979 |
} |
| 980 |
|
| 981 |
/** |
| 982 |
* Get enabled extensions with full config data |
| 983 |
* |
| 984 |
* @return array |
| 985 |
*/ |
| 986 |
public static function get_enabled_extensions() |
| 987 |
{ |
| 988 |
$enabled_settings = self::get_extensions() ?: []; |
| 989 |
$all_extensions = Config::get_extensions(); |
| 990 |
$enabled = []; |
| 991 |
|
| 992 |
foreach ($all_extensions as $key => $extension) { |
| 993 |
if (!empty($enabled_settings[$key])) { |
| 994 |
$enabled[$key] = $extension; |
| 995 |
} |
| 996 |
} |
| 997 |
|
| 998 |
return $enabled; |
| 999 |
} |
| 1000 |
|
| 1001 |
/** |
| 1002 |
* Get enabled extensions by group |
| 1003 |
* |
| 1004 |
* @param string $group Group key |
| 1005 |
* @return array |
| 1006 |
*/ |
| 1007 |
public static function get_enabled_extensions_by_group($group) |
| 1008 |
{ |
| 1009 |
$enabled_settings = self::get_extensions() ?: []; |
| 1010 |
$group_extensions = Config::get_extensions_by_group($group); |
| 1011 |
$enabled = []; |
| 1012 |
|
| 1013 |
foreach ($group_extensions as $key => $extension) { |
| 1014 |
if (!empty($enabled_settings[$key])) { |
| 1015 |
$enabled[$key] = $extension; |
| 1016 |
} |
| 1017 |
} |
| 1018 |
|
| 1019 |
return $enabled; |
| 1020 |
} |
| 1021 |
|
| 1022 |
/** |
| 1023 |
* Get addon counts by group (total and enabled) |
| 1024 |
* |
| 1025 |
* @return array ['group' => ['total' => n, 'enabled' => n]] |
| 1026 |
*/ |
| 1027 |
public static function get_addon_counts_by_group() |
| 1028 |
{ |
| 1029 |
$enabled_settings = self::get_addons() ?: []; |
| 1030 |
$groups = Config::get_groups(); |
| 1031 |
$counts = []; |
| 1032 |
|
| 1033 |
foreach (array_keys($groups) as $group) { |
| 1034 |
$group_addons = Config::get_addons_by_group($group); |
| 1035 |
$enabled = 0; |
| 1036 |
|
| 1037 |
foreach (array_keys($group_addons) as $key) { |
| 1038 |
if (!empty($enabled_settings[$key])) { |
| 1039 |
$enabled++; |
| 1040 |
} |
| 1041 |
} |
| 1042 |
|
| 1043 |
$counts[$group] = [ |
| 1044 |
'total' => count($group_addons), |
| 1045 |
'enabled' => $enabled, |
| 1046 |
]; |
| 1047 |
} |
| 1048 |
|
| 1049 |
return $counts; |
| 1050 |
} |
| 1051 |
|
| 1052 |
/** |
| 1053 |
* Get extension counts by group (total and enabled) |
| 1054 |
* |
| 1055 |
* @return array ['group' => ['total' => n, 'enabled' => n]] |
| 1056 |
*/ |
| 1057 |
public static function get_extension_counts_by_group() |
| 1058 |
{ |
| 1059 |
$enabled_settings = self::get_extensions() ?: []; |
| 1060 |
$extension_groups = Config::get_extension_groups(); |
| 1061 |
$counts = []; |
| 1062 |
|
| 1063 |
foreach (array_keys($extension_groups) as $group) { |
| 1064 |
$group_extensions = Config::get_extensions_by_group($group); |
| 1065 |
$enabled = 0; |
| 1066 |
|
| 1067 |
foreach (array_keys($group_extensions) as $key) { |
| 1068 |
if (!empty($enabled_settings[$key])) { |
| 1069 |
$enabled++; |
| 1070 |
} |
| 1071 |
} |
| 1072 |
|
| 1073 |
$counts[$group] = [ |
| 1074 |
'total' => count($group_extensions), |
| 1075 |
'enabled' => $enabled, |
| 1076 |
]; |
| 1077 |
} |
| 1078 |
|
| 1079 |
return $counts; |
| 1080 |
} |
| 1081 |
|
| 1082 |
/** |
| 1083 |
* Enable all addons in a group |
| 1084 |
* |
| 1085 |
* @param string $group Group key |
| 1086 |
* @return bool |
| 1087 |
*/ |
| 1088 |
public static function enable_group($group) |
| 1089 |
{ |
| 1090 |
$current = self::get_addons() ?: []; |
| 1091 |
$group_addons = Config::get_addons_by_group($group); |
| 1092 |
|
| 1093 |
foreach (array_keys($group_addons) as $key) { |
| 1094 |
$current[$key] = true; |
| 1095 |
} |
| 1096 |
|
| 1097 |
return self::save_addons($current); |
| 1098 |
} |
| 1099 |
|
| 1100 |
/** |
| 1101 |
* Disable all addons in a group |
| 1102 |
* |
| 1103 |
* @param string $group Group key |
| 1104 |
* @return bool |
| 1105 |
*/ |
| 1106 |
public static function disable_group($group) |
| 1107 |
{ |
| 1108 |
$current = self::get_addons() ?: []; |
| 1109 |
$group_addons = Config::get_addons_by_group($group); |
| 1110 |
|
| 1111 |
foreach (array_keys($group_addons) as $key) { |
| 1112 |
$current[$key] = false; |
| 1113 |
} |
| 1114 |
|
| 1115 |
return self::save_addons($current); |
| 1116 |
} |
| 1117 |
|
| 1118 |
/** |
| 1119 |
* Enable all extensions in a group |
| 1120 |
* |
| 1121 |
* @param string $group Group key |
| 1122 |
* @return bool |
| 1123 |
*/ |
| 1124 |
public static function enable_extension_group($group) |
| 1125 |
{ |
| 1126 |
$current = self::get_extensions() ?: []; |
| 1127 |
$group_extensions = Config::get_extensions_by_group($group); |
| 1128 |
|
| 1129 |
foreach (array_keys($group_extensions) as $key) { |
| 1130 |
$current[$key] = true; |
| 1131 |
} |
| 1132 |
|
| 1133 |
return self::save_extensions($current); |
| 1134 |
} |
| 1135 |
|
| 1136 |
/** |
| 1137 |
* Disable all extensions in a group |
| 1138 |
* |
| 1139 |
* @param string $group Group key |
| 1140 |
* @return bool |
| 1141 |
*/ |
| 1142 |
public static function disable_extension_group($group) |
| 1143 |
{ |
| 1144 |
$current = self::get_extensions() ?: []; |
| 1145 |
$group_extensions = Config::get_extensions_by_group($group); |
| 1146 |
|
| 1147 |
foreach (array_keys($group_extensions) as $key) { |
| 1148 |
$current[$key] = false; |
| 1149 |
} |
| 1150 |
|
| 1151 |
return self::save_extensions($current); |
| 1152 |
} |
| 1153 |
|
| 1154 |
/** |
| 1155 |
* Get default addon settings (all enabled) |
| 1156 |
* |
| 1157 |
* @return array |
| 1158 |
*/ |
| 1159 |
public static function get_default_addon_settings() |
| 1160 |
{ |
| 1161 |
$all_addons = Config::get_addons(); |
| 1162 |
$defaults = []; |
| 1163 |
|
| 1164 |
foreach (array_keys($all_addons) as $key) { |
| 1165 |
$defaults[$key] = true; |
| 1166 |
} |
| 1167 |
|
| 1168 |
return $defaults; |
| 1169 |
} |
| 1170 |
|
| 1171 |
/** |
| 1172 |
* Get default extension settings (all enabled except mega-menu) |
| 1173 |
* |
| 1174 |
* @return array |
| 1175 |
*/ |
| 1176 |
public static function get_default_extension_settings() |
| 1177 |
{ |
| 1178 |
$all_extensions = Config::get_extensions(); |
| 1179 |
$defaults = []; |
| 1180 |
|
| 1181 |
foreach (array_keys($all_extensions) as $key) { |
| 1182 |
$defaults[$key] = true; |
| 1183 |
} |
| 1184 |
|
| 1185 |
return $defaults; |
| 1186 |
} |
| 1187 |
|
| 1188 |
/** |
| 1189 |
* Get default plugin settings (all enabled) |
| 1190 |
* |
| 1191 |
* @return array |
| 1192 |
*/ |
| 1193 |
public static function get_default_plugin_settings() |
| 1194 |
{ |
| 1195 |
return array_fill_keys(array_keys(Config::get_plugins()), true); |
| 1196 |
} |
| 1197 |
|
| 1198 |
/** |
| 1199 |
* Get default icon library settings (all enabled) |
| 1200 |
* |
| 1201 |
* @return array |
| 1202 |
*/ |
| 1203 |
public static function get_default_icon_settings() |
| 1204 |
{ |
| 1205 |
return array_fill_keys(array_keys(Config::get_icons()), true); |
| 1206 |
} |
| 1207 |
|
| 1208 |
// ------------------------------------------------------------------------- |
| 1209 |
// Core get/save (public so SettingsProxy can call them) |
| 1210 |
// ------------------------------------------------------------------------- |
| 1211 |
|
| 1212 |
/** |
| 1213 |
* Generic get with migration support |
| 1214 |
* |
| 1215 |
* @param string $option_key The option key |
| 1216 |
* @param string|null $key Specific setting key |
| 1217 |
* @param mixed $default Default value |
| 1218 |
* @return mixed |
| 1219 |
*/ |
| 1220 |
public static function get_option($option_key, $key = null, $default = null) |
| 1221 |
{ |
| 1222 |
if (!self::is_valid_option_key($option_key)) { |
| 1223 |
return $key === null ? [] : $default; |
| 1224 |
} |
| 1225 |
|
| 1226 |
if (!isset(self::$cache[$option_key])) { |
| 1227 |
// Try to get from new key first |
| 1228 |
$settings = get_option($option_key, null); |
| 1229 |
|
| 1230 |
// If not found, check legacy key |
| 1231 |
if ($settings === null) { |
| 1232 |
$legacy_key = array_search($option_key, self::LEGACY_KEYS); |
| 1233 |
if ($legacy_key !== false) { |
| 1234 |
$settings = get_option($legacy_key, []); |
| 1235 |
// Migrate to new key if legacy data exists |
| 1236 |
if (!empty($settings)) { |
| 1237 |
update_option($option_key, $settings); |
| 1238 |
} |
| 1239 |
} |
| 1240 |
} |
| 1241 |
|
| 1242 |
self::$cache[$option_key] = $settings ?: []; |
| 1243 |
} |
| 1244 |
|
| 1245 |
$settings = self::$cache[$option_key]; |
| 1246 |
|
| 1247 |
if ($key === null) { |
| 1248 |
return $settings; |
| 1249 |
} |
| 1250 |
|
| 1251 |
return isset($settings[$key]) ? $settings[$key] : $default; |
| 1252 |
} |
| 1253 |
|
| 1254 |
/** |
| 1255 |
* Generic save method |
| 1256 |
* |
| 1257 |
* @param string $option_key The option key |
| 1258 |
* @param array $settings Settings array |
| 1259 |
* @return bool |
| 1260 |
*/ |
| 1261 |
public static function save_option($option_key, $settings) |
| 1262 |
{ |
| 1263 |
if (!self::is_valid_option_key($option_key)) { |
| 1264 |
return false; |
| 1265 |
} |
| 1266 |
|
| 1267 |
$settings = self::sanitize_settings($option_key, $settings); |
| 1268 |
|
| 1269 |
unset(self::$cache[$option_key]); |
| 1270 |
|
| 1271 |
return update_option($option_key, $settings); |
| 1272 |
} |
| 1273 |
|
| 1274 |
/** |
| 1275 |
* Clear settings cache |
| 1276 |
* |
| 1277 |
* @param string|null $option_key Specific key or null for all |
| 1278 |
* @return $this|void Returns $this when called on instance for chaining |
| 1279 |
*/ |
| 1280 |
public static function clear_cache($option_key = null) |
| 1281 |
{ |
| 1282 |
if ($option_key === null) { |
| 1283 |
self::$cache = []; |
| 1284 |
} else { |
| 1285 |
unset(self::$cache[$option_key]); |
| 1286 |
} |
| 1287 |
|
| 1288 |
if (self::$instance !== null) { |
| 1289 |
return self::$instance; |
| 1290 |
} |
| 1291 |
} |
| 1292 |
|
| 1293 |
/** |
| 1294 |
* Migrate all legacy settings to new keys |
| 1295 |
* |
| 1296 |
* @return array Migration results |
| 1297 |
*/ |
| 1298 |
public static function migrate_legacy_settings() |
| 1299 |
{ |
| 1300 |
$results = []; |
| 1301 |
|
| 1302 |
foreach (self::LEGACY_KEYS as $legacy_key => $new_key) { |
| 1303 |
$legacy_data = get_option($legacy_key, null); |
| 1304 |
|
| 1305 |
if ($legacy_data !== null) { |
| 1306 |
// Check if new key already has data |
| 1307 |
$new_data = get_option($new_key, null); |
| 1308 |
|
| 1309 |
if ($new_data === null) { |
| 1310 |
// Migrate data to new key |
| 1311 |
update_option($new_key, $legacy_data); |
| 1312 |
$results[$legacy_key] = 'migrated'; |
| 1313 |
} else { |
| 1314 |
$results[$legacy_key] = 'skipped (new key exists)'; |
| 1315 |
} |
| 1316 |
} else { |
| 1317 |
$results[$legacy_key] = 'no data'; |
| 1318 |
} |
| 1319 |
} |
| 1320 |
|
| 1321 |
return $results; |
| 1322 |
} |
| 1323 |
|
| 1324 |
/** |
| 1325 |
* Remove all legacy option keys |
| 1326 |
* |
| 1327 |
* @return array Deletion results |
| 1328 |
*/ |
| 1329 |
public static function remove_legacy_settings() |
| 1330 |
{ |
| 1331 |
$results = []; |
| 1332 |
|
| 1333 |
foreach (array_keys(self::LEGACY_KEYS) as $legacy_key) { |
| 1334 |
if (delete_option($legacy_key)) { |
| 1335 |
$results[$legacy_key] = 'deleted'; |
| 1336 |
} else { |
| 1337 |
$results[$legacy_key] = 'not found'; |
| 1338 |
} |
| 1339 |
} |
| 1340 |
|
| 1341 |
return $results; |
| 1342 |
} |
| 1343 |
|
| 1344 |
/** |
| 1345 |
* Get all option keys (new format) |
| 1346 |
* |
| 1347 |
* @return array |
| 1348 |
*/ |
| 1349 |
public static function get_option_keys() |
| 1350 |
{ |
| 1351 |
return [ |
| 1352 |
'addons' => self::ADDONS_KEY, |
| 1353 |
'extensions' => self::EXTENSIONS_KEY, |
| 1354 |
'plugins' => self::PLUGINS_KEY, |
| 1355 |
'icons' => self::ICONS_KEY, |
| 1356 |
'api' => self::API_KEY, |
| 1357 |
'white_label' => self::WHITE_LABEL, |
| 1358 |
]; |
| 1359 |
} |
| 1360 |
|
| 1361 |
// ------------------------------------------------------------------------- |
| 1362 |
// Validation & Sanitization helpers |
| 1363 |
// ------------------------------------------------------------------------- |
| 1364 |
|
| 1365 |
/** |
| 1366 |
* Check if an option exists in the database (vs being an empty array) |
| 1367 |
* |
| 1368 |
* Distinguishes "option was never saved" (first install) from |
| 1369 |
* "option was saved as empty array" (user disabled everything). |
| 1370 |
* |
| 1371 |
* @param string $option_key WP option key constant (e.g. Settings::ADDONS_KEY) |
| 1372 |
* @return bool |
| 1373 |
*/ |
| 1374 |
public static function option_exists($option_key) |
| 1375 |
{ |
| 1376 |
return get_option($option_key, null) !== null; |
| 1377 |
} |
| 1378 |
|
| 1379 |
/** |
| 1380 |
* Check whether an option key is one of the known constants |
| 1381 |
* |
| 1382 |
* @param string $option_key |
| 1383 |
* @return bool |
| 1384 |
*/ |
| 1385 |
private static function is_valid_option_key($option_key) |
| 1386 |
{ |
| 1387 |
return in_array($option_key, [ |
| 1388 |
self::ADDONS_KEY, |
| 1389 |
self::EXTENSIONS_KEY, |
| 1390 |
self::PLUGINS_KEY, |
| 1391 |
self::ICONS_KEY, |
| 1392 |
self::API_KEY, |
| 1393 |
], true); |
| 1394 |
} |
| 1395 |
|
| 1396 |
/** |
| 1397 |
* Context-aware sanitization based on option key |
| 1398 |
* |
| 1399 |
* Toggle maps (addons, extensions, plugins, icons): keys → sanitize_key(), values → (int) 0|1 |
| 1400 |
* API settings: keys → sanitize_key(), values → sanitize_text_field() |
| 1401 |
* |
| 1402 |
* @param string $option_key |
| 1403 |
* @param mixed $settings |
| 1404 |
* @return array |
| 1405 |
*/ |
| 1406 |
private static function sanitize_settings($option_key, $settings) |
| 1407 |
{ |
| 1408 |
if (!is_array($settings)) { |
| 1409 |
return []; |
| 1410 |
} |
| 1411 |
|
| 1412 |
$sanitized = []; |
| 1413 |
|
| 1414 |
if ($option_key === self::API_KEY) { |
| 1415 |
// API values may be nested (recaptcha: {site_key, secret_key}, twitter: {...}, etc.) |
| 1416 |
foreach ($settings as $key => $value) { |
| 1417 |
$sanitized[sanitize_key($key)] = self::sanitize_api_value($value); |
| 1418 |
} |
| 1419 |
} else { |
| 1420 |
// Toggle maps: cast to int 0|1 |
| 1421 |
foreach ($settings as $key => $value) { |
| 1422 |
$sanitized[sanitize_key($key)] = (int) (bool) $value; |
| 1423 |
} |
| 1424 |
} |
| 1425 |
|
| 1426 |
return $sanitized; |
| 1427 |
} |
| 1428 |
|
| 1429 |
/** |
| 1430 |
* Recursively sanitize API setting values |
| 1431 |
* |
| 1432 |
* Handles nested arrays (e.g. recaptcha: {site_key, secret_key}), |
| 1433 |
* strings, booleans, and numeric values. |
| 1434 |
* |
| 1435 |
* @param mixed $value |
| 1436 |
* @return mixed |
| 1437 |
*/ |
| 1438 |
private static function sanitize_api_value($value) |
| 1439 |
{ |
| 1440 |
if (is_array($value)) { |
| 1441 |
$result = []; |
| 1442 |
foreach ($value as $k => $v) { |
| 1443 |
$result[sanitize_key($k)] = self::sanitize_api_value($v); |
| 1444 |
} |
| 1445 |
return $result; |
| 1446 |
} |
| 1447 |
|
| 1448 |
if (is_bool($value)) { |
| 1449 |
return $value; |
| 1450 |
} |
| 1451 |
|
| 1452 |
if (is_numeric($value)) { |
| 1453 |
return $value; |
| 1454 |
} |
| 1455 |
|
| 1456 |
if (is_string($value)) { |
| 1457 |
return sanitize_text_field($value); |
| 1458 |
} |
| 1459 |
|
| 1460 |
return ''; |
| 1461 |
} |
| 1462 |
} |
| 1463 |
|