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

1,457 lines 47.8 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;
253 items.forEach(function(a) {
254 var href = a.getAttribute("href") || "";
255 if (href.indexOf("jltma-template-library") !== -1) libraryItem = a.parentElement;
256 if (href.indexOf("jltma-template-kits") !== -1) kitsItem = a.parentElement;
257 if (href.indexOf("master-addons-setup-wizard") !== -1) wizardItem = a.parentElement;
258 if (href.indexOf("master-addons-recommended") !== -1) recommendedItem = a.parentElement;
259 });
260 // Place Template Kits right after Template Library
261 if (libraryItem && kitsItem && libraryItem.nextElementSibling !== kitsItem) {
262 submenu.insertBefore(kitsItem, libraryItem.nextElementSibling);
263 }
264 // Move "Setup Wizard" just before "Recommended"
265 if (wizardItem && recommendedItem) {
266 submenu.insertBefore(wizardItem, recommendedItem);
267 }
268
269 // Separators and pricing
270 var separatorAfter = [
271 "page=master-addons-settings",
272 "jltma-template-kits",
273 "post_type=jltma_widget"
274 ];
275 submenu.querySelectorAll("li a").forEach(function(a) {
276 var href = a.getAttribute("href") || "";
277 for (var i = 0; i < separatorAfter.length; i++) {
278 var pos = href.indexOf(separatorAfter[i]);
279 if (pos === -1) { continue; }
280 // Require a param boundary after the match so
281 // "page=master-addons-settings" does NOT also match
282 // "page=master-addons-settings-account" (the Account item).
283 var nextChar = href.charAt(pos + separatorAfter[i].length);
284 if (nextChar === "" || nextChar === "&" || nextChar === "#" || nextChar === "\"") {
285 a.parentElement.classList.add("jltma-menu-separator");
286 break;
287 }
288 }
289 var text = (a.textContent || "").trim().toLowerCase();
290 if (href.indexOf("pricing") !== -1 || text.indexOf("pricing") !== -1 || text.indexOf("upgrade") !== -1) {
291 a.parentElement.classList.add("jltma-menu-pricing");
292 }
293 });
294 });
295 ';
296
297 // Load the menu styling and ordering through the core enqueue APIs
298 // (inline-only handles) instead of hardcoded <style>/<script> tags.
299 wp_register_style('jltma-admin-menu', false, array(), JLTMA_VER);
300 wp_enqueue_style('jltma-admin-menu');
301 wp_add_inline_style('jltma-admin-menu', $css);
302
303 wp_register_script('jltma-admin-menu', false, array(), JLTMA_VER, true);
304 wp_enqueue_script('jltma-admin-menu');
305 wp_add_inline_script('jltma-admin-menu', $js);
306 }
307
308 /**
309 * Admin Body Class
310 */
311 public function jltma_admin_body_class($class)
312 {
313 $bodyclass = '';
314 $bodyclass .= ' jltma-admin ';
315 return $class . $bodyclass;
316 }
317
318
319 public function jltma_admin_settings_scripts() {
320 $screen = get_current_screen();
321
322 $jltma_menu_label = __('Master Addons', 'master-addons');
323 $menu_label = apply_filters('master_addons/white_label/menu_label', $jltma_menu_label);
324
325 // Check if we're on any Master Addons admin page
326 $is_master_addons_page = (
327 $screen->id == 'toplevel_page_master-addons-settings' ||
328 $screen->id == 'toplevel_page_master-addons-settings-network' ||
329 // strpos($screen->id, 'master-addons_page_') === 0 ||
330 $screen->id === strtolower(preg_replace('/\s+/', '-', trim($menu_label))) . '_page_master-addons-settings'
331 // || (isset($screen->parent_base) && $screen->parent_base === 'master-addons-settings')
332 );
333
334 // Setup Wizard page
335 $is_setup_wizard_page = (
336 strpos($screen->id, 'master-addons-setup-wizard') !== false
337 );
338
339 // Load Scripts only Master Addons Admin Page
340 if ($is_master_addons_page && empty( $is_setup_wizard_page )) {
341
342 // Hide all admin notices on settings page
343 remove_all_actions('admin_notices');
344 remove_all_actions('all_admin_notices');
345 remove_all_actions('network_admin_notices');
346 remove_all_actions('user_admin_notices');
347 remove_action('admin_notices', 'update_nag', 3);
348 remove_action('admin_notices', 'maintenance_nag', 10);
349
350 // Hide any remaining admin notices on our settings screen. Attached to the
351 // settings stylesheet (enqueued below) instead of a hardcoded <style> tag.
352 $jltma_hide_notices_css = '
353 .notice, .error, .updated, .update-nag, .admin-notice,
354 .jltma-plugin-update-notice, .fs-notice, .fs-slug-master-addons,
355 #wpbody-content > .notice, #wpbody-content > .error, #wpbody-content > .updated,
356 .wrap > .notice, .wrap > .error, .wrap > .updated {
357 display: none !important;
358 }
359 #wpcontent {
360 padding-left: 0 !important;
361 }
362 #wpfooter {
363 display: none !important;
364 }';
365
366 // Dequeue Spectra (UAG) zipwp-images style to prevent CSS conflicts on settings page
367 add_action('admin_enqueue_scripts', function () {
368 wp_dequeue_style('zipwp-images-style');
369 }, 9999);
370
371 if (!did_action('wp_enqueue_media')) {
372 wp_enqueue_media();
373 }
374
375 // Elementor icons for addon card icons — use Elementor's copy if available, otherwise local fallback
376 if (wp_style_is('elementor-icons', 'registered')) {
377 wp_enqueue_style('elementor-icons');
378 } else {
379 wp_enqueue_style('jltma-elementor-icons');
380 }
381
382 // Only load React app on the React settings page to avoid Lucide icons conflicting with native Image constructor
383 wp_enqueue_style('jltma-admin-settings');
384 wp_enqueue_script('jltma-admin-settings');
385
386 wp_add_inline_style('jltma-admin-settings', $jltma_hide_notices_css);
387
388 // White Label logo & Hidden Nav Menus
389 $white_label_settings = jltma_settings()->get('jltma_white_label_settings');
390 $white_label_settings = is_array($white_label_settings) ? $white_label_settings : [];
391 $white_label_logo = '';
392
393 // White Label Logo
394 if( !empty(Utils::check_options($white_label_settings['jltma_wl_plugin_logo'] ?? '') ) ) {
395 $white_label_logo = wp_get_attachment_image_src($white_label_settings['jltma_wl_plugin_logo'])[0];
396 }
397
398 // Hidden Nav Menus — keys must match nav_menus.ts ids
399 $hidden_menus = [];
400 $tab_keys = [
401 'welcome' => 'jltma_wl_plugin_tab_welcome',
402 'addons' => 'jltma_wl_plugin_tab_addons',
403 'extensions' => 'jltma_wl_plugin_tab_extensions',
404 'tools' => 'jltma_wl_plugin_tab_tools',
405 'free_vs_pro' => 'jltma_wl_plugin_tab_free_vs_pro',
406 'white_label' => 'jltma_wl_plugin_tab_white_label',
407 'template_kits' => 'jltma_wl_plugin_tab_template_kits',
408 ];
409 foreach ($tab_keys as $menu_id => $db_key) {
410 $hidden_menus[$menu_id] = !empty($white_label_settings[$db_key]) ? true : false;
411 }
412
413 $current_user = wp_get_current_user();
414
415 $localize_data = [
416 'pluginSlug' => 'master-addons',
417 'restUrl' => rest_url('master-addons/v1'),
418 'nonce' => wp_create_nonce('wp_rest'),
419 'adminUrl' => admin_url(),
420 'assetsUrl' => JLTMA_ASSETS,
421 'logo' => array(
422 'light' => $white_label_logo ? $white_label_logo : JLTMA_ASSETS . 'images/full-logo.svg',
423 'dark' => JLTMA_ASSETS . 'images/full-logo.png',
424 ),
425 'darkMode' => 'light',
426 'data' => Config::get_config(),
427 'is_premium' => ma_el_fs()->can_use_premium_code__premium_only(),
428 'is_developer' => ma_el_fs()->is_plan__premium_only('developer'),
429 'hidden_menus' => $hidden_menus,
430 'is_setup_complete' => REST_API::is_setup_complete(),
431 'user_email' => $current_user->user_email,
432 'user_name' => $current_user->display_name,
433 'subscribe_nonce' => wp_create_nonce('jltma_subscribe_nonce'),
434 'version' => JLTMA_VER,
435 ];
436
437 wp_localize_script('jltma-admin-settings', 'JLTMA_SETTINGS', $localize_data);
438 }
439
440 if ($is_setup_wizard_page) {
441 $this->jltma_setup_wizard_scrips();
442 }
443
444 // Recommended Plugins page assets
445 $is_recommended_page = (
446 strpos($screen->id, 'master-addons-recommended-plugins') !== false
447 );
448 if ($is_recommended_page) {
449 wp_enqueue_style('jltma-recommended-plugins');
450 wp_enqueue_script('jltma-recommended-plugins');
451 }
452
453 // ADMIN SDK Localize
454 wp_enqueue_style('jltma-admin-sdk');
455 wp_enqueue_script('jltma-admin-sdk');
456
457 wp_localize_script(
458 'jltma-admin-sdk',
459 'JLTMACORE',
460 array(
461 'admin_ajax' => admin_url('admin-ajax.php'),
462 'recommended_nonce' => wp_create_nonce('jltma_recommended_nonce'),
463 'is_premium' => Helper::jltma_premium(),
464 )
465 );
466 }
467
468 /**
469 * Enqueue scripts and styles for the Setup Wizard page, and hide admin UI elements for a full-screen experience
470 * 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
471 */
472 public function jltma_setup_wizard_scrips() {
473 // Hide admin notices, admin bar, and sidebar for full-screen experience
474 remove_all_actions('admin_notices');
475 remove_all_actions('all_admin_notices');
476 remove_all_actions('network_admin_notices');
477 remove_all_actions('user_admin_notices');
478
479 // Hide the footer for the full-screen wizard (attached to the wizard
480 // stylesheet below instead of a hardcoded <style> tag).
481 $jltma_wizard_css = '#wpfooter { display: none !important; }';
482
483 // Elementor icons for addon card icons
484 if (wp_style_is('elementor-icons', 'registered')) {
485 wp_enqueue_style('elementor-icons');
486 } else {
487 wp_enqueue_style('jltma-elementor-icons');
488 }
489
490 wp_enqueue_style('jltma-setup-wizard');
491 wp_enqueue_script('jltma-setup-wizard');
492
493 wp_add_inline_style('jltma-setup-wizard', $jltma_wizard_css);
494
495 // Build recommended plugins data with pre-computed install status.
496 $recommended_instance = Recommended_Plugins::get_instance();
497 $raw_plugins = $recommended_instance->plugins_list();
498 $recommended_plugins = [];
499
500 if ( ! function_exists( 'install_plugin_install_status' ) ) {
501 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
502 }
503 if ( ! function_exists( 'is_plugin_active' ) ) {
504 require_once ABSPATH . 'wp-admin/includes/plugin.php';
505 }
506
507 foreach ( $raw_plugins as $slug => $plugin ) {
508 $plugin_api = (object) $plugin;
509 if ( ! isset( $plugin_api->version ) ) {
510 $plugin_api->version = '';
511 }
512 $install_status = \install_plugin_install_status( $plugin_api );
513
514 // Map install_plugin_install_status() result to simple status.
515 if ( 'install' === $install_status['status'] ) {
516 $status = 'not_installed';
517 $plugin_file = '';
518 } elseif ( ! empty( $install_status['file'] ) && is_plugin_active( $install_status['file'] ) ) {
519 $status = 'active';
520 $plugin_file = $install_status['file'];
521 } else {
522 $status = 'installed';
523 $plugin_file = $install_status['file'] ?? '';
524 }
525
526 $recommended_plugins[] = [
527 'slug' => $plugin['slug'],
528 'name' => $plugin['name'],
529 'icon' => $plugin['icon'],
530 'download_link' => $plugin['download_link'],
531 'status' => $status,
532 'plugin_file' => $plugin_file,
533 ];
534 }
535
536 $localize_data = [
537 'pluginSlug' => 'master-addons',
538 'restUrl' => rest_url('master-addons/v1'),
539 'nonce' => wp_create_nonce('wp_rest'),
540 'adminUrl' => admin_url(),
541 'ajaxUrl' => admin_url('admin-ajax.php'),
542 'recommended_nonce' => wp_create_nonce('jltma_recommended_nonce'),
543 'recommended_plugins' => $recommended_plugins,
544 'logo' => array(
545 'light' => JLTMA_ASSETS . 'images/full-logo.svg',
546 'dark' => JLTMA_ASSETS . 'images/full-logo.png',
547 ),
548 'darkMode' => 'light',
549 'data' => Config::get_config(),
550 'is_premium' => ma_el_fs()->can_use_premium_code__premium_only(),
551 'is_developer' => ma_el_fs()->is_plan__premium_only('developer'),
552 'hidden_menus' => [],
553 'step_details' => REST_API::get_instance()->get_setup_status()->data
554 ];
555
556 wp_localize_script('jltma-setup-wizard', 'JLTMA_SETUP_WIZARD', $localize_data);
557 }
558
559 /**
560 * Singleton instance
561 *
562 * @var self|null
563 */
564 private static $instance = null;
565
566 /**
567 * Cached SettingsProxy instances (one per group)
568 *
569 * @var SettingsProxy[]
570 */
571 private $proxies = [];
572
573
574 /**
575 * Register admin menu page
576 */
577 public function register_admin_menu()
578 {
579 $image_dir = defined('JLTMA_IMAGE_DIR') ? JLTMA_IMAGE_DIR : (defined('JLTMA_PRO_IMAGE_DIR') ? JLTMA_PRO_IMAGE_DIR : '');
580 $logo = $image_dir . 'red-logo.svg';
581 $page_title = __('Master Addons for Elementor', 'master-addons');
582 $menu_label = __('Master Addons', 'master-addons');
583
584 $logo = apply_filters('master_addons/white_label/menu_logo', $logo);
585 $page_title = apply_filters('master_addons/white_label/page_title', $page_title);
586 $menu_label = apply_filters('master_addons/white_label/menu_label', $menu_label);
587
588 // Update badge
589 $update_plugins = get_site_transient('update_plugins');
590 $has_update = (defined('JLTMA_BASE') && !empty($update_plugins->response[JLTMA_BASE]))
591 || (defined('JLTMA_PRO_BASE') && !empty($update_plugins->response[JLTMA_PRO_BASE]));
592
593 if ($has_update) {
594 $menu_label = sprintf('%s <span class="jltma-menu-notice">1</span>', $menu_label);
595 }
596
597 add_menu_page(
598 $page_title,
599 $menu_label,
600 'manage_options',
601 'master-addons-settings',
602 [$this, 'render_settings_page'],
603 $logo,
604 57
605 );
606
607 add_submenu_page(
608 'master-addons-settings',
609 $page_title,
610 __('Settings', 'master-addons'),
611 'manage_options',
612 'master-addons-settings',
613 [$this, 'render_settings_page']
614 );
615
616 // Setup Wizard - registered with empty parent for Freemius-independent access,
617 // plus a visible submenu link under Master Addons until completed
618 if ( ! REST_API::is_setup_complete() ) {
619 // Hidden page registration (always accessible regardless of Freemius state)
620 add_submenu_page(
621 '',
622 __('Setup Wizard', 'master-addons'),
623 __('Setup Wizard', 'master-addons'),
624 'manage_options',
625 'master-addons-setup-wizard',
626 [$this, 'jltma_setup_wizard_page_content']
627 );
628
629 // Visible menu link under Master Addons
630 global $submenu;
631 $submenu['master-addons-settings'][] = array(
632 __('Setup Wizard', 'master-addons'),
633 'manage_options',
634 'admin.php?page=master-addons-setup-wizard',
635 );
636 }
637 }
638
639 /**
640 * Render the settings page (loads welcome.php template)
641 */
642 public function render_settings_page()
643 {
644 echo '<div id="jltma-admin-settings-root"></div>';
645 }
646
647 /**
648 * Setup Wizard page content
649 */
650 public function jltma_setup_wizard_page_content()
651 {
652 echo '<div id="jltma-setup-wizard-root"></div>';
653 }
654
655 /**
656 * Get singleton instance
657 *
658 * @return self
659 */
660 public static function instance()
661 {
662 if (self::$instance === null) {
663 self::$instance = new self();
664 }
665
666 return self::$instance;
667 }
668
669 /**
670 * Generic option getter for any wp_options key
671 *
672 * Reads a raw WordPress option with in-memory caching.
673 * Works with any option key — not limited to PROXY_MAP groups.
674 *
675 * Usage:
676 * jltma_settings()->get('jltma_white_label_settings');
677 * jltma_settings()->get('jltma_white_label_settings', 'jltma_wl_plugin_logo');
678 * jltma_settings()->get('jltma_white_label_settings', 'missing_key', 'fallback');
679 *
680 * @param string $option_key WordPress option name
681 * @param string|null $sub_key Optional sub-key within the option array
682 * @param mixed $default Default value when key is missing
683 * @return mixed
684 */
685 public function get($option_key, $sub_key = null, $default = null)
686 {
687 if (!isset(self::$cache[$option_key])) {
688 self::$cache[$option_key] = get_option($option_key, []);
689 }
690
691 $settings = self::$cache[$option_key];
692
693 if ($sub_key === null) {
694 return $settings ?: $default;
695 }
696
697 return isset($settings[$sub_key]) ? $settings[$sub_key] : $default;
698 }
699
700 /**
701 * Magic property access → returns cached SettingsProxy
702 *
703 * Enables: Settings::instance()->addons->get('key')
704 *
705 * @param string $name Group name (addons, extensions, plugins, icons, api)
706 * @return SettingsProxy
707 * @throws \InvalidArgumentException If group name is invalid
708 */
709 public function __get($name)
710 {
711 if (!isset(self::PROXY_MAP[$name])) {
712 throw new \InvalidArgumentException(
713 esc_html(sprintf('Unknown settings group "%s". Valid groups: %s', $name, implode(', ', array_keys(self::PROXY_MAP))))
714 );
715 }
716
717 if (!isset($this->proxies[$name])) {
718 $this->proxies[$name] = new SettingsProxy($name, self::PROXY_MAP[$name]);
719 }
720
721 return $this->proxies[$name];
722 }
723
724 /**
725 * Static facade shorthand
726 *
727 * Enables: Settings::addons('key') as shorthand for Settings::instance()->addons->get('key')
728 *
729 * @param string $name Group name
730 * @param array $arguments [0] = key (optional), [1] = default (optional)
731 * @return mixed
732 */
733 public static function __callStatic($name, $arguments)
734 {
735 if (isset(self::PROXY_MAP[$name])) {
736 $proxy = self::instance()->$name;
737 $key = $arguments[0] ?? null;
738 $default = $arguments[1] ?? null;
739
740 return $proxy->get($key, $default);
741 }
742
743 throw new \BadMethodCallException(
744 esc_html(sprintf('Call to undefined method %s::%s()', static::class, $name))
745 );
746 }
747
748 // -------------------------------------------------------------------------
749 // Existing static API (unchanged signatures)
750 // -------------------------------------------------------------------------
751
752 /**
753 * Get addons settings (enabled/disabled state)
754 *
755 * @param string|null $key Specific addon key or null for all
756 * @param mixed $default Default value
757 * @return mixed
758 */
759 public static function get_addons($key = null, $default = null)
760 {
761 return self::get_option(self::ADDONS_KEY, $key, $default);
762 }
763
764 /**
765 * Save addons settings
766 *
767 * @param array $settings Settings array
768 * @return bool
769 */
770 public static function save_addons($settings)
771 {
772 return self::save_option(self::ADDONS_KEY, $settings);
773 }
774
775 /**
776 * Get extensions settings (enabled/disabled state)
777 *
778 * @param string|null $key Specific extension key or null for all
779 * @param mixed $default Default value
780 * @return mixed
781 */
782 public static function get_extensions($key = null, $default = null)
783 {
784 return self::get_option(self::EXTENSIONS_KEY, $key, $default);
785 }
786
787 /**
788 * Save extensions settings
789 *
790 * @param array $settings Settings array
791 * @return bool
792 */
793 public static function save_extensions($settings)
794 {
795 return self::save_option(self::EXTENSIONS_KEY, $settings);
796 }
797
798 /**
799 * Get third-party plugins settings
800 *
801 * @param string|null $key Specific plugin key or null for all
802 * @param mixed $default Default value
803 * @return mixed
804 */
805 public static function get_plugins($key = null, $default = null)
806 {
807 return self::get_option(self::PLUGINS_KEY, $key, $default);
808 }
809
810 /**
811 * Save third-party plugins settings
812 *
813 * @param array $settings Settings array
814 * @return bool
815 */
816 public static function save_plugins($settings)
817 {
818 return self::save_option(self::PLUGINS_KEY, $settings);
819 }
820
821 /**
822 * Get icons library settings
823 *
824 * @param string|null $key Specific icon key or null for all
825 * @param mixed $default Default value
826 * @return mixed
827 */
828 public static function get_icons($key = null, $default = null)
829 {
830 return self::get_option(self::ICONS_KEY, $key, $default);
831 }
832
833 /**
834 * Save icons library settings
835 *
836 * @param array $settings Settings array
837 * @return bool
838 */
839 public static function save_icons($settings)
840 {
841 return self::save_option(self::ICONS_KEY, $settings);
842 }
843
844 /**
845 * Get API settings
846 *
847 * @param string|null $key Specific API key or null for all
848 * @param mixed $default Default value
849 * @return mixed
850 */
851 public static function get_api($key = null, $default = null)
852 {
853 return self::get_option(self::API_KEY, $key, $default);
854 }
855
856 /**
857 * Save API settings
858 *
859 * @param array $settings Settings array
860 * @return bool
861 */
862 public static function save_api($settings)
863 {
864 return self::save_option(self::API_KEY, $settings);
865 }
866
867 /**
868 * Check if an addon is enabled
869 *
870 * @param string $addon_key
871 * @return bool
872 */
873 public static function is_addon_enabled($addon_key)
874 {
875 return (bool) self::get_addons($addon_key, true);
876 }
877
878 /**
879 * Check if an extension is enabled
880 *
881 * @param string $extension_key
882 * @return bool
883 */
884 public static function is_extension_enabled($extension_key)
885 {
886 return (bool) self::get_extensions($extension_key, true);
887 }
888
889 /**
890 * Check if a plugin integration is enabled
891 *
892 * @param string $plugin_key
893 * @return bool
894 */
895 public static function is_plugin_enabled($plugin_key)
896 {
897 return (bool) self::get_plugins($plugin_key, true);
898 }
899
900 /**
901 * Check if an icon library is enabled
902 *
903 * @param string $icon_key
904 * @return bool
905 */
906 public static function is_icon_enabled($icon_key)
907 {
908 return (bool) self::get_icons($icon_key, true);
909 }
910
911 /**
912 * Get enabled addons with full config data
913 * Merges enabled state with addon definitions from Config
914 *
915 * @return array
916 */
917 public static function get_enabled_addons()
918 {
919 $enabled_settings = self::get_addons() ?: [];
920 $all_addons = Config::get_addons();
921 $enabled = [];
922
923 foreach ($all_addons as $key => $addon) {
924 if (!empty($enabled_settings[$key])) {
925 $enabled[$key] = $addon;
926 }
927 }
928
929 return $enabled;
930 }
931
932 /**
933 * Get enabled addons by group
934 *
935 * @param string $group Group key
936 * @return array
937 */
938 public static function get_enabled_addons_by_group($group)
939 {
940 $enabled_settings = self::get_addons() ?: [];
941 $group_addons = Config::get_addons_by_group($group);
942 $enabled = [];
943
944 foreach ($group_addons as $key => $addon) {
945 if (!empty($enabled_settings[$key])) {
946 $enabled[$key] = $addon;
947 }
948 }
949
950 return $enabled;
951 }
952
953 /**
954 * Get enabled addons by group and subcategory
955 *
956 * @param string $group Group key
957 * @param string $subcategory Subcategory key
958 * @return array
959 */
960 public static function get_enabled_addons_by_subcategory($group, $subcategory)
961 {
962 $enabled_settings = self::get_addons() ?: [];
963 $subcategory_addons = Config::get_addons_by_subcategory($group, $subcategory);
964 $enabled = [];
965
966 foreach ($subcategory_addons as $key => $addon) {
967 if (!empty($enabled_settings[$key])) {
968 $enabled[$key] = $addon;
969 }
970 }
971
972 return $enabled;
973 }
974
975 /**
976 * Get enabled extensions with full config data
977 *
978 * @return array
979 */
980 public static function get_enabled_extensions()
981 {
982 $enabled_settings = self::get_extensions() ?: [];
983 $all_extensions = Config::get_extensions();
984 $enabled = [];
985
986 foreach ($all_extensions as $key => $extension) {
987 if (!empty($enabled_settings[$key])) {
988 $enabled[$key] = $extension;
989 }
990 }
991
992 return $enabled;
993 }
994
995 /**
996 * Get enabled extensions by group
997 *
998 * @param string $group Group key
999 * @return array
1000 */
1001 public static function get_enabled_extensions_by_group($group)
1002 {
1003 $enabled_settings = self::get_extensions() ?: [];
1004 $group_extensions = Config::get_extensions_by_group($group);
1005 $enabled = [];
1006
1007 foreach ($group_extensions as $key => $extension) {
1008 if (!empty($enabled_settings[$key])) {
1009 $enabled[$key] = $extension;
1010 }
1011 }
1012
1013 return $enabled;
1014 }
1015
1016 /**
1017 * Get addon counts by group (total and enabled)
1018 *
1019 * @return array ['group' => ['total' => n, 'enabled' => n]]
1020 */
1021 public static function get_addon_counts_by_group()
1022 {
1023 $enabled_settings = self::get_addons() ?: [];
1024 $groups = Config::get_groups();
1025 $counts = [];
1026
1027 foreach (array_keys($groups) as $group) {
1028 $group_addons = Config::get_addons_by_group($group);
1029 $enabled = 0;
1030
1031 foreach (array_keys($group_addons) as $key) {
1032 if (!empty($enabled_settings[$key])) {
1033 $enabled++;
1034 }
1035 }
1036
1037 $counts[$group] = [
1038 'total' => count($group_addons),
1039 'enabled' => $enabled,
1040 ];
1041 }
1042
1043 return $counts;
1044 }
1045
1046 /**
1047 * Get extension counts by group (total and enabled)
1048 *
1049 * @return array ['group' => ['total' => n, 'enabled' => n]]
1050 */
1051 public static function get_extension_counts_by_group()
1052 {
1053 $enabled_settings = self::get_extensions() ?: [];
1054 $extension_groups = Config::get_extension_groups();
1055 $counts = [];
1056
1057 foreach (array_keys($extension_groups) as $group) {
1058 $group_extensions = Config::get_extensions_by_group($group);
1059 $enabled = 0;
1060
1061 foreach (array_keys($group_extensions) as $key) {
1062 if (!empty($enabled_settings[$key])) {
1063 $enabled++;
1064 }
1065 }
1066
1067 $counts[$group] = [
1068 'total' => count($group_extensions),
1069 'enabled' => $enabled,
1070 ];
1071 }
1072
1073 return $counts;
1074 }
1075
1076 /**
1077 * Enable all addons in a group
1078 *
1079 * @param string $group Group key
1080 * @return bool
1081 */
1082 public static function enable_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] = true;
1089 }
1090
1091 return self::save_addons($current);
1092 }
1093
1094 /**
1095 * Disable all addons in a group
1096 *
1097 * @param string $group Group key
1098 * @return bool
1099 */
1100 public static function disable_group($group)
1101 {
1102 $current = self::get_addons() ?: [];
1103 $group_addons = Config::get_addons_by_group($group);
1104
1105 foreach (array_keys($group_addons) as $key) {
1106 $current[$key] = false;
1107 }
1108
1109 return self::save_addons($current);
1110 }
1111
1112 /**
1113 * Enable all extensions in a group
1114 *
1115 * @param string $group Group key
1116 * @return bool
1117 */
1118 public static function enable_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] = true;
1125 }
1126
1127 return self::save_extensions($current);
1128 }
1129
1130 /**
1131 * Disable all extensions in a group
1132 *
1133 * @param string $group Group key
1134 * @return bool
1135 */
1136 public static function disable_extension_group($group)
1137 {
1138 $current = self::get_extensions() ?: [];
1139 $group_extensions = Config::get_extensions_by_group($group);
1140
1141 foreach (array_keys($group_extensions) as $key) {
1142 $current[$key] = false;
1143 }
1144
1145 return self::save_extensions($current);
1146 }
1147
1148 /**
1149 * Get default addon settings (all enabled)
1150 *
1151 * @return array
1152 */
1153 public static function get_default_addon_settings()
1154 {
1155 $all_addons = Config::get_addons();
1156 $defaults = [];
1157
1158 foreach (array_keys($all_addons) as $key) {
1159 $defaults[$key] = true;
1160 }
1161
1162 return $defaults;
1163 }
1164
1165 /**
1166 * Get default extension settings (all enabled except mega-menu)
1167 *
1168 * @return array
1169 */
1170 public static function get_default_extension_settings()
1171 {
1172 $all_extensions = Config::get_extensions();
1173 $defaults = [];
1174
1175 foreach (array_keys($all_extensions) as $key) {
1176 $defaults[$key] = true;
1177 }
1178
1179 return $defaults;
1180 }
1181
1182 /**
1183 * Get default plugin settings (all enabled)
1184 *
1185 * @return array
1186 */
1187 public static function get_default_plugin_settings()
1188 {
1189 return array_fill_keys(array_keys(Config::get_plugins()), true);
1190 }
1191
1192 /**
1193 * Get default icon library settings (all enabled)
1194 *
1195 * @return array
1196 */
1197 public static function get_default_icon_settings()
1198 {
1199 return array_fill_keys(array_keys(Config::get_icons()), true);
1200 }
1201
1202 // -------------------------------------------------------------------------
1203 // Core get/save (public so SettingsProxy can call them)
1204 // -------------------------------------------------------------------------
1205
1206 /**
1207 * Generic get with migration support
1208 *
1209 * @param string $option_key The option key
1210 * @param string|null $key Specific setting key
1211 * @param mixed $default Default value
1212 * @return mixed
1213 */
1214 public static function get_option($option_key, $key = null, $default = null)
1215 {
1216 if (!self::is_valid_option_key($option_key)) {
1217 return $key === null ? [] : $default;
1218 }
1219
1220 if (!isset(self::$cache[$option_key])) {
1221 // Try to get from new key first
1222 $settings = get_option($option_key, null);
1223
1224 // If not found, check legacy key
1225 if ($settings === null) {
1226 $legacy_key = array_search($option_key, self::LEGACY_KEYS);
1227 if ($legacy_key !== false) {
1228 $settings = get_option($legacy_key, []);
1229 // Migrate to new key if legacy data exists
1230 if (!empty($settings)) {
1231 update_option($option_key, $settings);
1232 }
1233 }
1234 }
1235
1236 self::$cache[$option_key] = $settings ?: [];
1237 }
1238
1239 $settings = self::$cache[$option_key];
1240
1241 if ($key === null) {
1242 return $settings;
1243 }
1244
1245 return isset($settings[$key]) ? $settings[$key] : $default;
1246 }
1247
1248 /**
1249 * Generic save method
1250 *
1251 * @param string $option_key The option key
1252 * @param array $settings Settings array
1253 * @return bool
1254 */
1255 public static function save_option($option_key, $settings)
1256 {
1257 if (!self::is_valid_option_key($option_key)) {
1258 return false;
1259 }
1260
1261 $settings = self::sanitize_settings($option_key, $settings);
1262
1263 unset(self::$cache[$option_key]);
1264
1265 return update_option($option_key, $settings);
1266 }
1267
1268 /**
1269 * Clear settings cache
1270 *
1271 * @param string|null $option_key Specific key or null for all
1272 * @return $this|void Returns $this when called on instance for chaining
1273 */
1274 public static function clear_cache($option_key = null)
1275 {
1276 if ($option_key === null) {
1277 self::$cache = [];
1278 } else {
1279 unset(self::$cache[$option_key]);
1280 }
1281
1282 if (self::$instance !== null) {
1283 return self::$instance;
1284 }
1285 }
1286
1287 /**
1288 * Migrate all legacy settings to new keys
1289 *
1290 * @return array Migration results
1291 */
1292 public static function migrate_legacy_settings()
1293 {
1294 $results = [];
1295
1296 foreach (self::LEGACY_KEYS as $legacy_key => $new_key) {
1297 $legacy_data = get_option($legacy_key, null);
1298
1299 if ($legacy_data !== null) {
1300 // Check if new key already has data
1301 $new_data = get_option($new_key, null);
1302
1303 if ($new_data === null) {
1304 // Migrate data to new key
1305 update_option($new_key, $legacy_data);
1306 $results[$legacy_key] = 'migrated';
1307 } else {
1308 $results[$legacy_key] = 'skipped (new key exists)';
1309 }
1310 } else {
1311 $results[$legacy_key] = 'no data';
1312 }
1313 }
1314
1315 return $results;
1316 }
1317
1318 /**
1319 * Remove all legacy option keys
1320 *
1321 * @return array Deletion results
1322 */
1323 public static function remove_legacy_settings()
1324 {
1325 $results = [];
1326
1327 foreach (array_keys(self::LEGACY_KEYS) as $legacy_key) {
1328 if (delete_option($legacy_key)) {
1329 $results[$legacy_key] = 'deleted';
1330 } else {
1331 $results[$legacy_key] = 'not found';
1332 }
1333 }
1334
1335 return $results;
1336 }
1337
1338 /**
1339 * Get all option keys (new format)
1340 *
1341 * @return array
1342 */
1343 public static function get_option_keys()
1344 {
1345 return [
1346 'addons' => self::ADDONS_KEY,
1347 'extensions' => self::EXTENSIONS_KEY,
1348 'plugins' => self::PLUGINS_KEY,
1349 'icons' => self::ICONS_KEY,
1350 'api' => self::API_KEY,
1351 'white_label' => self::WHITE_LABEL,
1352 ];
1353 }
1354
1355 // -------------------------------------------------------------------------
1356 // Validation & Sanitization helpers
1357 // -------------------------------------------------------------------------
1358
1359 /**
1360 * Check if an option exists in the database (vs being an empty array)
1361 *
1362 * Distinguishes "option was never saved" (first install) from
1363 * "option was saved as empty array" (user disabled everything).
1364 *
1365 * @param string $option_key WP option key constant (e.g. Settings::ADDONS_KEY)
1366 * @return bool
1367 */
1368 public static function option_exists($option_key)
1369 {
1370 return get_option($option_key, null) !== null;
1371 }
1372
1373 /**
1374 * Check whether an option key is one of the known constants
1375 *
1376 * @param string $option_key
1377 * @return bool
1378 */
1379 private static function is_valid_option_key($option_key)
1380 {
1381 return in_array($option_key, [
1382 self::ADDONS_KEY,
1383 self::EXTENSIONS_KEY,
1384 self::PLUGINS_KEY,
1385 self::ICONS_KEY,
1386 self::API_KEY,
1387 ], true);
1388 }
1389
1390 /**
1391 * Context-aware sanitization based on option key
1392 *
1393 * Toggle maps (addons, extensions, plugins, icons): keys → sanitize_key(), values → (int) 0|1
1394 * API settings: keys → sanitize_key(), values → sanitize_text_field()
1395 *
1396 * @param string $option_key
1397 * @param mixed $settings
1398 * @return array
1399 */
1400 private static function sanitize_settings($option_key, $settings)
1401 {
1402 if (!is_array($settings)) {
1403 return [];
1404 }
1405
1406 $sanitized = [];
1407
1408 if ($option_key === self::API_KEY) {
1409 // API values may be nested (recaptcha: {site_key, secret_key}, twitter: {...}, etc.)
1410 foreach ($settings as $key => $value) {
1411 $sanitized[sanitize_key($key)] = self::sanitize_api_value($value);
1412 }
1413 } else {
1414 // Toggle maps: cast to int 0|1
1415 foreach ($settings as $key => $value) {
1416 $sanitized[sanitize_key($key)] = (int) (bool) $value;
1417 }
1418 }
1419
1420 return $sanitized;
1421 }
1422
1423 /**
1424 * Recursively sanitize API setting values
1425 *
1426 * Handles nested arrays (e.g. recaptcha: {site_key, secret_key}),
1427 * strings, booleans, and numeric values.
1428 *
1429 * @param mixed $value
1430 * @return mixed
1431 */
1432 private static function sanitize_api_value($value)
1433 {
1434 if (is_array($value)) {
1435 $result = [];
1436 foreach ($value as $k => $v) {
1437 $result[sanitize_key($k)] = self::sanitize_api_value($v);
1438 }
1439 return $result;
1440 }
1441
1442 if (is_bool($value)) {
1443 return $value;
1444 }
1445
1446 if (is_numeric($value)) {
1447 return $value;
1448 }
1449
1450 if (is_string($value)) {
1451 return sanitize_text_field($value);
1452 }
1453
1454 return '';
1455 }
1456 }
1457