PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.0.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.0.9
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / admin / settings / settings.php

settings.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.0.9, at inc/admin/settings/settings.php

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