PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.2.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.2.2
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.2.2, at inc/admin/settings/settings.php

1,463 lines 48.2 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 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