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 / class-master-elementor-addons.php

class-master-elementor-addons.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.2, at class-master-elementor-addons.php

1,618 lines 56.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;
4
5 use MasterAddons\Inc\Admin\Config;
6 use MasterAddons\Inc\Admin\REST_API;
7 use MasterAddons\Inc\Admin\Settings\Settings;
8 use MasterAddons\Inc\Classes\Helper;
9 use MasterAddons\Inc\Classes\Feedback;
10 use MasterAddons\Inc\Classes\Freemius_Hooks;
11 use MasterAddons\Inc\Classes\Pro_Upgrade;
12 use MasterAddons\Inc\Classes\Recommended_Plugins;
13 use MasterAddons\Inc\Classes\Utils;
14 use MasterAddons\Inc\Admin\Promote_Pro_Addons;
15
16 if (!defined('ABSPATH')) {
17 exit;
18 } // No, Direct access Sir !!!
19
20 if (!class_exists('Master_Elementor_Addons')) {
21 /**
22 * Base class for Master Addons
23 * Can be extended by Pro version for additional features
24 * Removed 'final' to allow Pro extension
25 */
26 class Master_Elementor_Addons
27 {
28
29 static public $class_namespace = '\\MasterAddons\\Inc\\Classes\\';
30 public $controls_manager;
31
32 const VERSION = JLTMA_VER;
33
34 const MINIMUM_PHP_VERSION = '7.0';
35 const MINIMUM_ELEMENTOR_VERSION = '3.5.0';
36 const MINIMUM_PRO_VERSION = '3.0.0';
37
38 // Changed from private to protected to allow Pro class extension
39 protected $_localize_settings = [];
40 protected $reflection;
41 protected static $instance = null;
42 protected $jltma_classes = array();
43
44
45 public static function get_instance()
46 {
47 if (!self::$instance) {
48 self::$instance = new self;
49 self::$instance->jltma_init();
50 }
51 return self::$instance;
52 }
53
54
55 public function __construct()
56 {
57 $this->reflection = new \ReflectionClass($this);
58 $this->jltma_register_autoloader();
59
60 // Register deprecated Elementor class stubs to prevent fatal errors
61 // from third-party themes/plugins using removed Scheme_Color/Scheme_Typography
62 $this->jltma_register_elementor_compat_stubs();
63
64 $this->jltma_include_files();
65
66 // Load textdomain for translations
67 add_action('init', [ $this, 'load_textdomain' ]);
68 // Initialize Plugin
69 add_action('plugins_loaded', [$this, 'jltma_plugins_loaded']);
70
71 //Hook: elementor/elements/categories_register
72 // add_action('elementor/init', [$this, 'jltma_add_category_to_editor']);
73
74 add_action('elementor/init', [$this, 'jltma_add_actions_to_elementor'], 0);
75
76 // Add Elementor Widgets
77 add_action('elementor/widgets/register', [$this, 'jltma_init_widgets']);
78 add_action('elementor/elements/categories_registered', [$this, 'jltma_add_category_to_editor']);
79
80
81 // Register Controls - Must run for both Free and Pro versions
82 add_action('elementor/controls/register', [$this, 'jltma_register_controls']);
83
84 //Body Class
85 add_action('body_class', [$this, 'jltma_body_class']);
86
87 // AJAX handler for plugin install/activate
88 add_action('wp_ajax_jltma_plugin_action', [$this, 'jltma_ajax_plugin_action']);
89
90 add_filter('plugin_action_links_' . JLTMA_BASE, array($this, 'plugin_action_links'));
91 add_filter('network_admin_plugin_action_links_' . JLTMA_BASE, array($this, 'plugin_action_links'));
92 }
93
94 public function jltma_init()
95 {
96 $this->jltma_image_size();
97
98 //Redirect Hook
99 add_action('admin_init', [$this, 'jltma_add_redirect_hook']);
100
101 // Run pending upgrade migrations (e.g. legacy option key migration)
102 add_action('admin_init', [$this, 'jltma_maybe_run_upgrades'], 5);
103 }
104
105 /**
106 * Run pending upgrade scripts if the stored version is older than the current version.
107 */
108 public function jltma_maybe_run_upgrades()
109 {
110 $upgrades = new \MasterAddons\Inc\Classes\Upgrades();
111 if ($upgrades->if_updates_available()) {
112 $upgrades->run_updates();
113 }
114 }
115
116 public static function jltma_elementor()
117 {
118 return \Elementor\Plugin::$instance;
119 }
120
121 // Deactivation Hook
122 public static function jltma_plugin_deactivation_hook()
123 {
124 delete_option('jltma_activation_time');
125 }
126
127 // Activation Hook
128 public static function jltma_plugin_activation_hook()
129 {
130
131 self::activated_widgets();
132 self::activated_extensions();
133 self::activated_third_party_plugins();
134 self::activated_icons_library();
135
136 // Current Master Addons Version
137 $current_version = get_option('_master_addons_version', null);
138 if (is_null($current_version)) {
139 update_option('_master_addons_version', JLTMA_VER);
140 }
141
142 $jltma_white_label_setting = Utils::get_options('jltma_white_label_settings') ?? [];
143 if( !empty($jltma_white_label_setting) && isset($jltma_white_label_setting['jltma_wl_plugin_tab_white_label']) ) {
144 $jltma_white_label_setting['jltma_wl_plugin_tab_white_label'] = 0;
145 update_option( 'jltma_white_label_settings', $jltma_white_label_setting );
146 }
147
148 // Only auto-redirect to setup wizard for fresh installs or users already on 3.0.0+
149 // Old users upgrading from < 3.0.0 should NOT see the auto-redirect
150 $should_redirect = false;
151 if ( is_null( $current_version ) ) {
152 // Fresh install — show wizard
153 $should_redirect = true;
154 } elseif ( version_compare( $current_version, '3.0.0', '>=' ) ) {
155 // Re-activation of 3.0.0+ — show wizard if not completed
156 $should_redirect = true;
157 }
158
159 if ( $should_redirect ) {
160 set_transient( '_master_addons_activation_redirect', true, 30 );
161 }
162 }
163
164 public function set_plugin_activation_time()
165 {
166
167 if (is_multisite()) {
168
169 if (get_site_option('jltma_activation_time') === false) {
170
171 if (!function_exists('is_plugin_active_for_network')) {
172 require_once(ABSPATH . '/wp-admin/includes/plugin.php');
173 }
174
175 if (is_plugin_active_for_network('master-addons-pro/master-addons.php') || is_plugin_active_for_network('master-addons/master-addons.php')) {
176 update_site_option('jltma_activation_time', strtotime("now"));
177 }
178 }
179 } else {
180 if (get_option('jltma_activation_time') === false) {
181 update_option('jltma_activation_time', strtotime("now"));
182 }
183 }
184 }
185
186
187 // Initialize
188 /**
189 * Register stub classes for deprecated Elementor Scheme_Color / Scheme_Typography.
190 * Prevents fatal errors from third-party themes/plugins that still use them
191 * (e.g. Brooklyn Lite, themes built for older Elementor).
192 */
193 private function jltma_register_elementor_compat_stubs() {
194 \MasterAddons\Inc\Classes\Elementor_Compat::init();
195 }
196
197 public function jltma_plugins_loaded()
198 {
199 $this->set_plugin_activation_time();
200
201 // Check if Elementor installed and activated
202 if (!did_action('elementor/loaded')) {
203 add_action('admin_notices', array($this, 'jltma_admin_notice_missing_main_plugin'));
204 return;
205 }
206
207 // Check for required Elementor version
208 if (defined('ELEMENTOR_VERSION') && !version_compare(ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=')) {
209 add_action('admin_notices', array($this, 'jltma_admin_notice_minimum_elementor_version'));
210 return;
211 }
212
213 // Check for required PHP version
214 if (version_compare(PHP_VERSION, self::MINIMUM_PHP_VERSION, '<')) {
215 add_action('admin_notices', array($this, 'jltma_admin_notice_minimum_php_version'));
216 return;
217 }
218
219 // self::jltma_plugin_activation_hook();
220 }
221
222 /**
223 * Register autoloader
224 *
225 * @since 2.1.0
226 */
227 public function jltma_register_autoloader()
228 {
229 spl_autoload_register([$this, 'jltma_autoload']);
230 }
231
232 /**
233 * Autoload classes, traits, and interfaces
234 *
235 * Converts namespace to file path (WordPress style: kebab-case):
236 * - MasterAddons\Inc\Classes\Cache_Manager → inc/classes/cache-manager.php
237 * - MasterAddons\Inc\Traits\Swiper_Controls → inc/traits/swiper-controls.php
238 * - MasterAddons\Addons\MA_Accordion → addons/ma-accordion.php
239 *
240 * @param string $class Fully qualified class name
241 */
242 public function jltma_autoload($class)
243 {
244 // Only handle MasterAddons namespace
245 if (0 !== strpos($class, __NAMESPACE__)) {
246 return;
247 }
248
249 // Skip if already loaded
250 if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) {
251 return;
252 }
253
254 // Get relative class name (without MasterAddons\ prefix)
255 $relative_class = str_replace(__NAMESPACE__ . '\\', '', $class);
256
257 // Handle Pro\Modules\{Category}\{Class} pattern
258 if (preg_match('/^Pro\\\\Modules\\\\([^\\\\]+)\\\\([^\\\\]+)$/', $relative_class, $matches)) {
259 $category = $this->to_kebab_case($matches[1]);
260 $class_name = $this->to_kebab_case($matches[2]);
261 $file = JLTMA_PATH . "premium/modules/{$category}/{$class_name}/{$class_name}.php";
262
263 if (file_exists($file)) {
264 require_once $file;
265 return;
266 }
267 }
268
269 // Handle Modules\{Category}\{Class} pattern (free)
270 if (preg_match('/^Modules\\\\([^\\\\]+)\\\\([^\\\\]+)$/', $relative_class, $matches)) {
271 $category = $this->to_kebab_case($matches[1]);
272 $class_name = $this->to_kebab_case($matches[2]);
273 $file = JLTMA_PATH . "inc/modules/{$category}/{$class_name}/{$class_name}.php";
274
275 if (file_exists($file)) {
276 require_once $file;
277 return;
278 }
279 }
280
281 // Convert namespace to file path (kebab-case)
282 $file_name = strtolower(
283 preg_replace(
284 ['/([a-z])([A-Z])/', '/_/', '/\\\\/'],
285 ['$1-$2', '-', '/'],
286 $relative_class
287 )
288 );
289
290 // Inc\Classes folder
291 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Classes\\')) {
292 $file = JLTMA_PATH . $file_name . '.php';
293 if (is_readable($file)) {
294 include_once $file;
295 return;
296 }
297 }
298
299 // Inc\Controls folder
300 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Controls\\')) {
301 $file = JLTMA_PATH . $file_name . '.php';
302 if (is_readable($file)) {
303 include_once $file;
304 return;
305 }
306 }
307
308 // Inc\Modules folder
309 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Modules\\')) {
310 $file = JLTMA_PATH . $file_name . '.php';
311 if (is_readable($file)) {
312 include_once $file;
313 return;
314 }
315 }
316
317 // Inc\Admin\Templates folder (directory: inc/admin/templates/)
318 // NOTE: Must come BEFORE general Inc\Admin check
319 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Admin\\Templates\\')) {
320 $templates_class = str_replace(__NAMESPACE__ . '\\Inc\\Admin\\Templates\\', '', $class);
321
322 // Handle Kits subnamespace
323 if (0 === strpos($templates_class, 'Kits\\')) {
324 $kits_class = str_replace('Kits\\', '', $templates_class);
325 $kits_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $kits_class));
326 $file = JLTMA_PATH . 'inc/admin/templates/kits/class-' . $kits_file_name . '.php';
327 if (is_readable($file)) {
328 include_once $file;
329 return;
330 }
331 }
332
333 // Handle Includes subnamespace (types, classes, sources, documents)
334 if (0 === strpos($templates_class, 'Includes\\')) {
335 $includes_class = str_replace('Includes\\', '', $templates_class);
336 $includes_file_name = strtolower(
337 preg_replace(
338 ['/([a-z])([A-Z])/', '/_/', '/\\\\/'],
339 ['$1-$2', '-', '/'],
340 $includes_class
341 )
342 );
343 $file = JLTMA_PATH . 'inc/admin/templates/includes/' . $includes_file_name . '.php';
344 if (is_readable($file)) {
345 include_once $file;
346 return;
347 }
348 }
349
350 $templates_file_name = strtolower(
351 preg_replace(
352 ['/([a-z])([A-Z])/', '/_/', '/\\\\/'],
353 ['$1-$2', '-', '/'],
354 $templates_class
355 )
356 );
357 $file = JLTMA_PATH . 'inc/admin/templates/' . $templates_file_name . '.php';
358 if (is_readable($file)) {
359 include_once $file;
360 return;
361 }
362 }
363
364 // Inc\Admin\Theme_Builder folder (directory: inc/admin/theme-builder/)
365 // NOTE: Must come BEFORE general Inc\Admin check
366 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Admin\\Theme_Builder\\')) {
367 $tb_class = str_replace(__NAMESPACE__ . '\\Inc\\Admin\\Theme_Builder\\', '', $class);
368
369 // Handle subnamespaces: Api, Comments, Hooks
370 if (0 === strpos($tb_class, 'Api\\')) {
371 $api_class = str_replace('Api\\', '', $tb_class);
372 $api_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $api_class));
373 $file = JLTMA_PATH . 'inc/admin/theme-builder/inc/api/' . $api_file_name . '.php';
374 if (is_readable($file)) {
375 include_once $file;
376 return;
377 }
378 } elseif (0 === strpos($tb_class, 'Comments\\')) {
379 $comments_class = str_replace('Comments\\', '', $tb_class);
380 // Handle Addon subnamespace
381 if (0 === strpos($comments_class, 'Addon\\')) {
382 $addon_class = str_replace('Addon\\', '', $comments_class);
383 $file = JLTMA_PATH . 'inc/admin/theme-builder/inc/comments/jltma-comments-addon.php';
384 } else {
385 $comments_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $comments_class));
386 $file = JLTMA_PATH . 'inc/admin/theme-builder/inc/comments/class-' . $comments_file_name . '.php';
387 }
388 if (is_readable($file)) {
389 include_once $file;
390 return;
391 }
392 } elseif (0 === strpos($tb_class, 'Theme_Hooks\\')) {
393 $hooks_class = str_replace('Theme_Hooks\\', '', $tb_class);
394 $hooks_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $hooks_class));
395 $file = JLTMA_PATH . 'inc/admin/theme-builder/inc/theme-hooks/' . $hooks_file_name . '.php';
396 if (is_readable($file)) {
397 include_once $file;
398 return;
399 }
400 } else {
401 // Main Theme_Builder classes
402 $tb_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $tb_class));
403 // Try class-{name}.php format first
404 $file = JLTMA_PATH . 'inc/admin/theme-builder/class-' . $tb_file_name . '.php';
405 if (is_readable($file)) {
406 include_once $file;
407 return;
408 }
409 // Try inc/{name}.php format
410 $file = JLTMA_PATH . 'inc/admin/theme-builder/inc/' . $tb_file_name . '.php';
411 if (is_readable($file)) {
412 include_once $file;
413 return;
414 }
415 // Try specific file mappings
416 $tb_file_map = [
417 'Loader' => 'theme-builder.php',
418 'Theme_Builder' => 'class-theme-builder.php',
419 'CPT' => 'inc/cpt.php',
420 'CPT_Hooks' => 'inc/cpt-hooks.php',
421 'Activator' => 'inc/jltma-activator.php',
422 'Assets' => 'inc/theme-builder-assets.php',
423 ];
424 if (isset($tb_file_map[$tb_class])) {
425 $file = JLTMA_PATH . 'inc/admin/theme-builder/' . $tb_file_map[$tb_class];
426 if (is_readable($file)) {
427 include_once $file;
428 return;
429 }
430 }
431 }
432 }
433
434 // Inc\Admin\PopupBuilder folder (directory: inc/admin/popup-builder/)
435 // NOTE: Must come BEFORE general Inc\Admin check
436 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Admin\\PopupBuilder\\')) {
437 $pb_class = str_replace(__NAMESPACE__ . '\\Inc\\Admin\\PopupBuilder\\', '', $class);
438 $pb_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $pb_class));
439
440 $file = JLTMA_PATH . 'inc/admin/popup-builder/class-' . $pb_file_name . '.php';
441 if (is_readable($file)) {
442 include_once $file;
443 return;
444 }
445 }
446
447 // Inc\Admin\WidgetBuilder folder (directory: inc/admin/widget-builder/)
448 // NOTE: Must come BEFORE general Inc\Admin check
449 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Admin\\WidgetBuilder\\')) {
450 $wb_class = str_replace(__NAMESPACE__ . '\\Inc\\Admin\\WidgetBuilder\\', '', $class);
451
452 // Handle Controls subnamespace
453 if (0 === strpos($wb_class, 'Controls\\')) {
454 $control_class = str_replace('Controls\\', '', $wb_class);
455 $control_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $control_class));
456 $file = JLTMA_PATH . 'inc/admin/widget-builder/controls/' . $control_file_name . '.php';
457 if (is_readable($file)) {
458 include_once $file;
459 return;
460 }
461 }
462
463 // File mapping for widget-builder classes
464 $wb_file_map = [
465 'Widget_Builder_Init' => 'class-widget-builder-init.php',
466 'Widget_CPT' => 'class-widget-cpt.php',
467 'Widget_Admin' => 'class-widget-admin.php',
468 'Widget_Generator' => 'class-widget-generator.php',
469 'REST_Controller' => 'class-rest-controller.php',
470 'Shortcode_Manager' => 'class-shortcode-manager.php',
471 'Control_Manager' => 'class-control-manager.php',
472 'Icon_Library_Helper' => 'icon-library-helper.php',
473 'Control_Base' => 'controls/class-control-base.php',
474 ];
475 if (isset($wb_file_map[$wb_class])) {
476 $file = JLTMA_PATH . 'inc/admin/widget-builder/' . $wb_file_map[$wb_class];
477 if (is_readable($file)) {
478 include_once $file;
479 return;
480 }
481 }
482
483 // Fallback: Try kebab-case filename
484 $wb_file_name = strtolower(preg_replace(['/([a-z])([A-Z])/', '/_/'], ['$1-$2', '-'], $wb_class));
485 $file = JLTMA_PATH . 'inc/admin/widget-builder/' . $wb_file_name . '.php';
486 if (is_readable($file)) {
487 include_once $file;
488 return;
489 }
490 }
491
492 // Inc\Admin folder (general - must come AFTER specific Inc\Admin\* checks)
493 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Admin\\')) {
494 $file = JLTMA_PATH . $file_name . '.php';
495 if (is_readable($file)) {
496 include_once $file;
497 return;
498 }
499 }
500
501 // Inc\Traits folder
502 if (0 === strpos($class, __NAMESPACE__ . '\\Inc\\Traits\\')) {
503 $file = JLTMA_PATH . $file_name . '.php';
504 if (is_readable($file)) {
505 include_once $file;
506 return;
507 }
508 }
509
510 // Lib folder (lib/*.php)
511 if (0 === strpos($class, __NAMESPACE__ . '\\Lib\\')) {
512 $lib_class = str_replace(__NAMESPACE__ . '\\Lib\\', '', $class);
513 $lib_file_name = strtolower(
514 preg_replace(
515 ['/([a-z])([A-Z])/', '/_/'],
516 ['$1-$2', '-'],
517 $lib_class
518 )
519 );
520 $file = JLTMA_PATH . 'lib/' . $lib_file_name . '.php';
521 if (is_readable($file)) {
522 include_once $file;
523 return;
524 }
525 // Try PascalCase filename (Featured.php, etc.)
526 $file = JLTMA_PATH . 'lib/' . $lib_class . '.php';
527 if (is_readable($file)) {
528 include_once $file;
529 return;
530 }
531 }
532
533 // Pro\Classes folder (premium classes: premium/classes/)
534 if (0 === strpos($class, __NAMESPACE__ . '\\Pro\\Classes\\')) {
535 $pro_class = str_replace(__NAMESPACE__ . '\\Pro\\Classes\\', '', $class);
536 $pro_file_name = strtolower(
537 preg_replace(
538 ['/([a-z])([A-Z])/', '/_/', '/\\\\/'],
539 ['$1-$2', '-', '/'],
540 $pro_class
541 )
542 );
543 $file = JLTMA_PATH . 'premium/classes/' . $pro_file_name . '.php';
544 if (is_readable($file)) {
545 include_once $file;
546 return;
547 }
548 }
549
550
551 // Addons folder - organized by groups/subcategories (group/subcategory/widget/)
552 if (0 === strpos($class, __NAMESPACE__ . '\\Addons\\')) {
553 // Get class name and convert to file name
554 $class_name = str_replace(__NAMESPACE__ . '\\Addons\\', '', $class);
555 $addon_file_name = strtolower(
556 preg_replace(
557 ['/([a-z])([A-Z])/', '/_/'],
558 ['$1-$2', '-'],
559 $class_name
560 )
561 );
562 // Scan addons directory for group/subcategory subdirectories
563 $addons_dir = JLTMA_PATH . 'addons/';
564 if (is_dir($addons_dir)) {
565 $groups = array_filter(glob($addons_dir . '*'), 'is_dir');
566 foreach ($groups as $group_path) {
567 // Check in subcategories (group/subcategory/widget/)
568 $subcategories = array_filter(glob($group_path . '/*'), 'is_dir');
569 foreach ($subcategories as $subcategory_path) {
570 $file = $subcategory_path . '/' . $addon_file_name . '/' . $addon_file_name . '.php';
571 if (is_readable($file)) {
572 include_once $file;
573 return;
574 }
575 }
576 }
577 }
578 }
579
580 // Modules folder - organized by groups/subcategories (group/subcategory/module/)
581 if (0 === strpos($class, __NAMESPACE__ . '\\Modules\\')) {
582 $class_name = str_replace(__NAMESPACE__ . '\\Modules\\', '', $class);
583 $module_file_name = strtolower(
584 preg_replace(
585 ['/([a-z])([A-Z])/', '/_/'],
586 ['$1-$2', '-'],
587 $class_name
588 )
589 );
590 // Remove 'extension-' prefix if present for folder lookup
591 $folder_name = str_replace('extension-', '', $module_file_name);
592 // Scan modules directory for group/subcategory subdirectories
593 $modules_dir = JLTMA_PATH . 'inc/modules/';
594 if (is_dir($modules_dir)) {
595 $groups = array_filter(glob($modules_dir . '*'), 'is_dir');
596 foreach ($groups as $group_path) {
597 // Check in subcategories (group/subcategory/module/)
598 $subcategories = array_filter(glob($group_path . '/*'), 'is_dir');
599 foreach ($subcategories as $subcategory_path) {
600 $file = $subcategory_path . '/' . $folder_name . '/' . $folder_name . '.php';
601 if (is_readable($file)) {
602 include_once $file;
603 return;
604 }
605 }
606 }
607 }
608 }
609
610 // Fallback: try direct path
611 $file = JLTMA_PATH . $file_name . '.php';
612 if (is_readable($file)) {
613 include_once $file;
614 }
615 }
616
617 /**
618 * Convert PascalCase to kebab-case
619 *
620 * @param string $string PascalCase string
621 * @return string kebab-case string
622 */
623 private function to_kebab_case($string)
624 {
625 return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $string));
626 }
627
628 function jltma_add_category_to_editor($widgets_manager)
629 {
630
631 $widgets_manager->add_category(
632 'master-addons',
633 [
634 'title' => esc_html__('Master Addons', 'master-addons'),
635 'icon' => 'font',
636 ],
637 1
638 );
639 }
640
641 public function jltma_image_size()
642 {
643 add_image_size('master_addons_team_thumb', 250, 330, true);
644 }
645
646 // Widget Elements
647 public static function activated_widgets()
648 {
649 // get_addons() migrates from legacy key automatically.
650 // Only seed defaults on a genuine first install (no new AND no legacy key).
651 $data = Settings::get_addons();
652 if (empty($data)) {
653 $data = Settings::get_default_addon_settings();
654 Settings::save_addons($data);
655 }
656 return $data;
657 }
658
659 // Extensions
660 public static function activated_extensions()
661 {
662 $data = Settings::get_extensions();
663 if (empty($data)) {
664 $data = Settings::get_default_extension_settings();
665 Settings::save_extensions($data);
666 }
667 return $data;
668 }
669
670
671 // Third Party Plugins
672 public static function activated_third_party_plugins()
673 {
674 $data = Settings::get_plugins();
675 if (empty($data)) {
676 $data = Settings::get_default_plugin_settings();
677 Settings::save_plugins($data);
678 }
679 return $data;
680 }
681
682 // Icons Library
683 public static function activated_icons_library()
684 {
685 $data = Settings::get_icons();
686 if (empty($data)) {
687 $data = Settings::get_default_icon_settings();
688 Settings::save_icons($data);
689 }
690 return $data;
691 }
692
693 /**
694 * Load all extension classes and instance them.
695 *
696 * This method will:
697 * 1. Get all PHP files inside the inc/classes directory.
698 * 2. Include all of them.
699 * 3. Create an instance of each class.
700 * 4. Store the instance in the $jltma_classes property.
701 *
702 * @since 1.1.0
703 * @return void
704 */
705 public function jltma_add_actions_to_elementor() {
706 // Ensure all settings exist in DB (seeds defaults on first install).
707 // These are no-ops when the option already exists, so the overhead is negligible.
708 self::activated_third_party_plugins();
709 self::activated_icons_library();
710
711 // Load extensions (extension-prototype.php is loaded in jltma_include_files)
712 $this->jltma_load_extensions();
713
714 // Icons Extended lives outside the extensions_category groups,
715 // so register_extensions() doesn't pick it up. Load it directly.
716 $activated_extensions = Settings::get_extensions() ?: [];
717 if (!isset($activated_extensions['icons_extended']) || $activated_extensions['icons_extended']) {
718 $icons_file = JLTMA_PATH . 'inc/modules/utilities/icons-extended/icons-extended.php';
719 if (file_exists($icons_file)) {
720 require_once $icons_file;
721 }
722 }
723 }
724
725 public function jltma_register_controls($controls_manager)
726 {
727
728 $controls_manager = \Elementor\Plugin::$instance->controls_manager;
729
730 $controls = array(
731 'jltma-visual-select' => array(
732 'file' => JLTMA_PATH . 'inc/controls/visual-select.php',
733 'class' => 'MasterAddons\Inc\Controls\JLTMA_Visual_Select',
734 'type' => 'single'
735 ),
736 'jltma-transitions' => array(
737 'file' => JLTMA_PATH . 'inc/controls/group/transitions.php',
738 'class' => 'MasterAddons\Inc\Controls\Group\JLTMA_Transition',
739 'type' => 'group'
740 ),
741 'jltma-filters-hsb' => array(
742 'file' => JLTMA_PATH . 'inc/controls/group/filters-hsb.php',
743 'class' => 'MasterAddons\Inc\Controls\Group\JLTMA_Filters_HSB',
744 'type' => 'group'
745 ),
746 'jltma-button-background' => array(
747 'file' => JLTMA_PATH . 'inc/controls/group/button-background.php',
748 'class' => 'MasterAddons\Inc\Controls\Group\JLTMA_Button_Background',
749 'type' => 'group'
750 ),
751 'jltma-choose-text' => array(
752 'file' => JLTMA_PATH . 'inc/controls/choose-text.php',
753 'class' => 'MasterAddons\Inc\Controls\JLTMA_Choose_Text',
754 'type' => 'single'
755 ),
756 'jltma-file-select' => array(
757 'file' => JLTMA_PATH . 'inc/controls/file-select.php',
758 'class' => 'MasterAddons\Inc\Controls\JLTMA_File_Select',
759 'type' => 'single'
760 ),
761 'jltma_query' => array(
762 'file' => JLTMA_PATH . 'inc/controls/query.php',
763 'class' => 'MasterAddons\Inc\Controls\JLTMA_Query',
764 'type' => 'single'
765 ),
766 'jltma-template-controls' => array(
767 'file' => JLTMA_PATH . 'inc/controls/templates/template-controls.php',
768 'class' => 'MasterAddons\Inc\Controls\Templates\JLTMA_Template_Controls',
769 'type' => 'template'
770 ),
771
772 );
773
774 foreach ($controls as $control_type => $control_info) {
775 if (!empty($control_info['file']) && !empty($control_info['class'])) {
776
777 include_once($control_info['file']);
778
779 if (class_exists($control_info['class'])) {
780 $class_name = $control_info['class'];
781 } elseif (class_exists(__NAMESPACE__ . '\\' . $control_info['class'])) {
782 $class_name = __NAMESPACE__ . '\\' . $control_info['class'];
783 }
784
785 if ($control_info['type'] === 'group') {
786 $controls_manager->add_group_control($control_type, new $class_name());
787 } elseif ($control_info['type'] === 'template') {
788 // Template classes are just included, not registered with Elementor
789 continue;
790 } else {
791 $controls_manager->register(new $class_name());
792 }
793 }
794 }
795 }
796
797 public function get_widgets()
798 {
799 return [];
800 }
801
802 public function jltma_init_widgets()
803 {
804 $activated_widgets = self::activated_widgets();
805 $is_premium = Helper::jltma_premium();
806
807 // Network Check
808 if (defined('NETWORK_ACTIVATED') && JLTMA_NETWORK_ACTIVATED) {
809 global $wpdb;
810 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- direct query required for multisite blog enumeration; result is not user-supplied
811 $wpdb->prepare(
812 "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND spam = '0' AND deleted = '0' AND archived = '0'",
813 $wpdb->siteid
814 )
815 );
816 $original_blog_id = get_current_blog_id();
817
818 foreach ($blogs as $blog_id) {
819 switch_to_blog($blog_id->blog_id);
820 $this->register_widgets($activated_widgets, $is_premium);
821 }
822 switch_to_blog($original_blog_id);
823 } else {
824 $this->register_widgets($activated_widgets, $is_premium);
825 }
826 }
827
828 private function register_widgets($activated_widgets, $is_premium)
829 {
830 $widget_manager = Helper::jltma_elementor()->widgets_manager;
831 $jltma_all_addons = Config::get_all_elements_for_settings();
832 ksort($jltma_all_addons);
833
834 foreach ($jltma_all_addons as $key => $widget) {
835 if (!isset($activated_widgets[$widget['key']]) || !$activated_widgets[$widget['key']]) {
836 continue;
837 }
838
839 $is_pro_widget = isset($widget['is_pro']) && $widget['is_pro'];
840
841 // Skip pro widgets if user doesn't have premium
842 if ($is_pro_widget && !$is_premium) {
843 continue;
844 }
845
846 // Skip pro widgets if the pro base class isn't available
847 // (e.g., pro autoloader not registered due to version mismatch)
848 if ($is_pro_widget && !class_exists('\\MasterAddons\\Pro\\Classes\\Base\\Master_Widgets_Pro')) {
849 continue;
850 }
851
852 // Determine widget file path (group/subcategory/widget/)
853 $group = isset($widget['group']) ? $widget['group'] : '';
854 $subcategory = isset($widget['subcategory']) ? $widget['subcategory'] : '';
855 $widget_key = $widget['key'];
856 $widget_file = null;
857
858 // Search using group from config
859 if ($group) {
860 $search_paths = [];
861
862 // Try direct path first (group/widget/) - handles flat folder structures
863 $search_paths[] = JLTMA_ADDONS . $group . '/' . $widget_key . '/' . $widget_key . '.php';
864 if (defined('JLTMA_PRO_ADDONS')) {
865 $search_paths[] = JLTMA_PRO_ADDONS . $group . '/' . $widget_key . '/' . $widget_key . '.php';
866 }
867
868 // Then try full subcategory path if subcategory is set (group/subcategory/widget/)
869 if ($subcategory && $group !== $subcategory) {
870 $search_paths[] = JLTMA_ADDONS . $group . '/' . $subcategory . '/' . $widget_key . '/' . $widget_key . '.php';
871 if (defined('JLTMA_PRO_ADDONS')) {
872 $search_paths[] = JLTMA_PRO_ADDONS . $group . '/' . $subcategory . '/' . $widget_key . '/' . $widget_key . '.php';
873 }
874 }
875
876 foreach ($search_paths as $path) {
877 if (file_exists($path)) {
878 $widget_file = $path;
879 break;
880 }
881 }
882 }
883
884 // Dynamic scan if not found (search group/widget/ and group/subcat/widget/ patterns)
885 if (!$widget_file) {
886 $base_dirs = [JLTMA_ADDONS];
887 if (defined('JLTMA_PRO_ADDONS')) {
888 $base_dirs[] = JLTMA_PRO_ADDONS;
889 }
890 foreach ($base_dirs as $base_dir) {
891 if (!is_dir($base_dir)) continue;
892 $groups = array_filter(glob($base_dir . '*'), 'is_dir');
893 foreach ($groups as $group_path) {
894 // First try direct: group/widget/widget.php
895 $direct_path = $group_path . '/' . $widget_key . '/' . $widget_key . '.php';
896 if (file_exists($direct_path)) {
897 $widget_file = $direct_path;
898 break 2;
899 }
900 // Then try nested: group/subcat/widget/widget.php
901 $subdirs = array_filter(glob($group_path . '/*'), 'is_dir');
902 foreach ($subdirs as $subdir_path) {
903 $path = $subdir_path . '/' . $widget_key . '/' . $widget_key . '.php';
904 if (file_exists($path)) {
905 $widget_file = $path;
906 break 3;
907 }
908 }
909 }
910 }
911 }
912
913 if ($widget_file) {
914 require_once $widget_file;
915 $class_name = $widget['class'];
916 if (class_exists($class_name)) {
917 $widget_manager->register(new $class_name);
918 }
919 }
920 }
921 }
922
923
924
925
926 public function jltma_load_extensions()
927 {
928 $activated_extensions = self::activated_extensions();
929 $is_premium = Helper::jltma_premium();
930
931 // Network Check
932 if (defined('NETWORK_ACTIVATED') && JLTMA_NETWORK_ACTIVATED) {
933 global $wpdb;
934 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- direct query required for multisite blog enumeration; result is not user-supplied
935 $wpdb->prepare(
936 "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND spam = '0' AND deleted = '0' AND archived = '0'",
937 $wpdb->siteid
938 )
939 );
940 $original_blog_id = get_current_blog_id();
941
942 foreach ($blogs as $blog_id) {
943 switch_to_blog($blog_id->blog_id);
944 $this->register_extensions($activated_extensions, $is_premium);
945 }
946 switch_to_blog($original_blog_id);
947 } else {
948 $this->register_extensions($activated_extensions, $is_premium);
949 }
950 }
951
952 private function register_extensions($activated_extensions, $is_premium)
953 {
954 $extensions = Config::get_extensions();
955 ksort($extensions);
956
957 foreach ($extensions as $key => $extension) {
958 // Treat missing keys as enabled (default is all-on; missing = newly added)
959 if (isset($activated_extensions[$key]) && !$activated_extensions[$key]) {
960 continue;
961 }
962
963 $is_pro_extension = isset($extension['is_pro']) && $extension['is_pro'];
964
965 // Skip pro extensions if user doesn't have premium
966 if ($is_pro_extension && !$is_premium) {
967 continue;
968 }
969
970 // Determine extension file path (group/extension/)
971 $group = isset($extension['group']) ? $extension['group'] : '';
972 $subcategory = isset($extension['subcategory']) ? $extension['subcategory'] : '';
973
974 $base_paths = [];
975 if ($is_premium && $is_pro_extension) {
976 // In Freemius code split, JLTMA_PRO_DIR points to the pro plugin
977 // while JLTMA_PRO_EXTENSIONS may point to the free plugin's non-existent premium dir
978 if (defined('JLTMA_PRO_DIR')) {
979 $base_paths[] = JLTMA_PRO_DIR . 'premium/modules/';
980 }
981 if (defined('JLTMA_PRO_EXTENSIONS') && !in_array(JLTMA_PRO_EXTENSIONS, $base_paths, true)) {
982 $base_paths[] = JLTMA_PRO_EXTENSIONS;
983 }
984 }
985 if (empty($base_paths)) {
986 $base_paths = [JLTMA_PATH . 'inc/modules/'];
987 }
988
989 $extension_file = null;
990
991 // Search using group from config
992 if ($group) {
993 foreach ($base_paths as $base_path) {
994 // Try direct path first: group/extension/extension.php
995 $path = $base_path . $group . '/' . $key . '/' . $key . '.php';
996 if (file_exists($path)) {
997 $extension_file = $path;
998 break;
999 }
1000 // Try with subcategory if set: group/subcategory/extension/extension.php
1001 if ($subcategory && $group !== $subcategory) {
1002 $path = $base_path . $group . '/' . $subcategory . '/' . $key . '/' . $key . '.php';
1003 if (file_exists($path)) {
1004 $extension_file = $path;
1005 break;
1006 }
1007 }
1008 }
1009 }
1010
1011 // Dynamic scan if not found (search group/extension/ and group/subcat/extension/ patterns)
1012 if (!$extension_file) {
1013 foreach ($base_paths as $base_path) {
1014 if (!is_dir($base_path)) continue;
1015 $groups = array_filter(glob($base_path . '*'), 'is_dir');
1016 foreach ($groups as $group_path) {
1017 // First try direct: group/extension/extension.php
1018 $direct_path = $group_path . '/' . $key . '/' . $key . '.php';
1019 if (file_exists($direct_path)) {
1020 $extension_file = $direct_path;
1021 break 2;
1022 }
1023 // Then try nested: group/subcat/extension/extension.php
1024 $subdirs = array_filter(glob($group_path . '/*'), 'is_dir');
1025 foreach ($subdirs as $subdir_path) {
1026 $path = $subdir_path . '/' . $key . '/' . $key . '.php';
1027 if (file_exists($path)) {
1028 $extension_file = $path;
1029 break 3;
1030 }
1031 }
1032 }
1033 }
1034 }
1035
1036 if ($extension_file) {
1037 require_once $extension_file;
1038 }
1039 }
1040 }
1041
1042 public function jltma_editor_scripts_enqueue_js()
1043 {
1044
1045 wp_enqueue_script('ma-el-rellaxjs-lib', JLTMA_URL . '/assets/vendor/rellax/rellax.min.js', array('jquery'), self::VERSION, true);
1046 }
1047
1048 public function jltma_editor_scripts_css()
1049 {
1050 wp_enqueue_style('master-addons-editor', JLTMA_URL . '/assets/css/master-addons-editor.css', array(), JLTMA_VER);
1051 }
1052
1053
1054
1055
1056 public function is_elementor_activated($plugin_path = 'elementor/elementor.php')
1057 {
1058 $installed_plugins_list = get_plugins();
1059
1060 return isset($installed_plugins_list[$plugin_path]);
1061 }
1062
1063
1064 /*
1065 * Activation Plugin redirect hook
1066 */
1067 public function jltma_add_redirect_hook()
1068 {
1069 // Skip redirects on AJAX/CLI
1070 if (wp_doing_ajax() || (defined('WP_CLI') && WP_CLI)) {
1071 return;
1072 }
1073
1074 // Setup Wizard redirect — only on first activation via transient.
1075 // After activation, users can freely navigate; wizard menu stays visible until completed.
1076 if (apply_filters('jltma/setup_wizard_run', true) && get_transient('_master_addons_activation_redirect') && !REST_API::is_setup_complete()) {
1077
1078 // Don't redirect on multi-plugin activation
1079 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of a WordPress-set admin query var for activation-redirect flow; not form processing.
1080 if (isset($_GET['activate-multi'])) {
1081 delete_transient('_master_addons_activation_redirect');
1082 return;
1083 }
1084
1085 // Already on the wizard page — don't redirect loop
1086 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of the current admin page query var; not form processing.
1087 if (isset($_GET['page']) && $_GET['page'] === 'master-addons-setup-wizard') {
1088 delete_transient('_master_addons_activation_redirect');
1089 return;
1090 }
1091
1092 // Consume activation transient and redirect to wizard
1093 delete_transient('_master_addons_activation_redirect');
1094
1095 wp_safe_redirect(admin_url('admin.php?page=master-addons-setup-wizard'));
1096 exit;
1097 }
1098
1099 // Legacy redirect
1100 if (is_plugin_active('elementor/elementor.php')) {
1101 if (get_option('ma_el_update_redirect', false)) {
1102 delete_option('ma_el_update_redirect');
1103 delete_transient('ma_el_update_redirect');
1104 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of a WordPress-set admin query var for activation-redirect flow; not form processing.
1105 if (!isset($_GET['activate-multi']) && $this->is_elementor_activated()) {
1106 wp_safe_redirect('admin.php?page=master-addons-settings');
1107 exit;
1108 }
1109 }
1110 }
1111 }
1112
1113
1114 public function plugin_action_links($links) {
1115
1116 $links['settings'] = apply_filters(
1117 'jltma_settings_link',
1118 sprintf('<a class="master-addons-settings" href="%1$s">%2$s</a>', admin_url('admin.php?page=master-addons-settings'), __('Settings', 'master-addons'))
1119 );
1120
1121 return apply_filters('master_addons/plugin_links', $links);
1122 }
1123
1124
1125 /**
1126 * Initialize core classes and load non-autoloadable files
1127 * Classes/traits are autoloaded - this only handles initialization and bootstrapping files
1128 */
1129 public function jltma_include_files()
1130 {
1131 // Bootstrap file that initializes template system (calls master_addons_templates())
1132 require_once JLTMA_PATH . 'inc/admin/templates/templates.php';
1133
1134 // Initialize singletons (autoloader loads the classes)
1135 \MasterAddons\Inc\Classes\Background_Task_Manager::get_instance();
1136 add_filter('cron_schedules', [\MasterAddons\Inc\Classes\Background_Task_Manager::get_instance(), 'add_cron_intervals']);
1137 \MasterAddons\Inc\Classes\Assets_Manager::get_instance(); // Register vendor assets (always needed)
1138 \MasterAddons\Inc\Classes\Assets_Loader::get_instance();
1139 \MasterAddons\Inc\Classes\Cache_Manager::get_instance();
1140 \MasterAddons\Inc\Classes\Template_Library_Cache::get_instance();
1141 \MasterAddons\Inc\Classes\Template_Kit_Cache::get_instance();
1142 \MasterAddons\Inc\Classes\Ajax_Queries::get_instance();
1143 Promote_Pro_Addons::get_instance();
1144
1145 Recommended_Plugins::get_instance();
1146
1147 \MasterAddons\Inc\Admin\Theme_Builder\Loader::get_instance();
1148 \MasterAddons\Inc\Admin\PopupBuilder\Popup_Builder_Init::get_instance();
1149 \MasterAddons\Inc\Admin\WidgetBuilder\Widget_Builder_Init::get_instance();
1150 REST_API::get_instance();
1151
1152 \MasterAddons\Inc\Admin\Page_Importer::get_instance();
1153
1154 // Admin Settings
1155 new \MasterAddons\Inc\Admin\Settings\Settings();
1156
1157 // Freemius hooks (filters, submenu reorder, etc.) — must load for all users
1158 Freemius_Hooks::get_instance();
1159
1160 // Feedback dialog (deactivation survey) — loads for all users (CSS + dialog)
1161 new Feedback();
1162
1163 // Admin notifications (latest updates, rating, subscribe, etc.)
1164 // Deferred to init so textdomain is loaded before any __() calls.
1165 add_action('init', function () {
1166 new \MasterAddons\Inc\Classes\Notifications\Notifications();
1167 });
1168
1169 // Dashboard widget for all users
1170 $pro_upgrade = new Pro_Upgrade();
1171
1172 // Conditional loading based on plan (classes autoloaded via Lib namespace)
1173 if (!Helper::jltma_premium()) {
1174 \MasterAddons\Lib\Featured::get_instance();
1175 // Image Optimizer for free users — commented out, will release later
1176 // \MasterAddons\Inc\Admin\Image_Optimizer::get_instance();
1177 }
1178 }
1179
1180
1181 public function jltma_body_class($classes)
1182 {
1183 global $pagenow;
1184
1185 if (in_array($pagenow, ['post.php', 'post-new.php'], true) && \Elementor\Utils::is_post_support()) {
1186 $post = get_post();
1187
1188 $mode_class = \Elementor\Plugin::$instance->db->is_built_with_elementor($post->ID) ? 'elementor-editor-active' : 'elementor-editor-inactive master-addons';
1189
1190 $classes .= ' ' . $mode_class;
1191 }
1192
1193 return $classes;
1194 }
1195
1196
1197 public function get_localize_settings()
1198 {
1199 return $this->_localize_settings;
1200 }
1201
1202 public function add_localize_settings($setting_key, $setting_value = null)
1203 {
1204 if (is_array($setting_key)) {
1205 $this->_localize_settings = array_replace_recursive($this->_localize_settings, $setting_key);
1206
1207 return;
1208 }
1209
1210 if (!is_array($setting_value) || !isset($this->_localize_settings[$setting_key]) || !is_array($this->_localize_settings[$setting_key])) {
1211 $this->_localize_settings[$setting_key] = $setting_value;
1212
1213 return;
1214 }
1215
1216 $this->_localize_settings[$setting_key] = array_replace_recursive($this->_localize_settings[$setting_key], $setting_value);
1217 }
1218
1219
1220
1221 /**
1222 * Check if running as premium-only version (master-addons-pro)
1223 *
1224 * @return bool
1225 */
1226 public function is_premium_only_version()
1227 {
1228 // Check if the plugin basename contains 'master-addons-pro'
1229 return (strpos(JLTMA_BASE, 'master-addons-pro/') === 0);
1230 }
1231
1232
1233
1234 /**
1235 * AJAX handler for plugin install/activate
1236 */
1237 public function jltma_ajax_plugin_action()
1238 {
1239 // Verify nonce
1240 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'jltma_plugin_action')) {
1241 wp_send_json_error(__('Security check failed.', 'master-addons'));
1242 }
1243
1244 $plugin_action = isset($_POST['plugin_action']) ? sanitize_text_field( wp_unslash( $_POST['plugin_action'] ) ) : '';
1245 $plugin_slug = isset($_POST['plugin']) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
1246
1247 if (empty($plugin_action) || empty($plugin_slug)) {
1248 wp_send_json_error(__('Invalid request.', 'master-addons'));
1249 }
1250
1251 // Plugin file mapping
1252 $plugin_files = [
1253 'elementor' => 'elementor/elementor.php',
1254 'master-addons' => 'master-addons/master-addons.php',
1255 ];
1256
1257 $plugin_file = isset($plugin_files[$plugin_slug]) ? $plugin_files[$plugin_slug] : $plugin_slug . '/' . $plugin_slug . '.php';
1258
1259 if ($plugin_action === 'install') {
1260 // Check permission
1261 if (!current_user_can('install_plugins')) {
1262 wp_send_json_error(__('You do not have permission to install plugins.', 'master-addons'));
1263 }
1264
1265 // Include required files
1266 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
1267 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1268 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
1269 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
1270
1271 // Get plugin info from WordPress.org
1272 $api = plugins_api('plugin_information', [
1273 'slug' => $plugin_slug,
1274 'fields' => [
1275 'short_description' => false,
1276 'sections' => false,
1277 'requires' => false,
1278 'rating' => false,
1279 'ratings' => false,
1280 'downloaded' => false,
1281 'last_updated' => false,
1282 'added' => false,
1283 'tags' => false,
1284 'compatibility' => false,
1285 'homepage' => false,
1286 'donate_link' => false,
1287 ],
1288 ]);
1289
1290 if (is_wp_error($api)) {
1291 wp_send_json_error($api->get_error_message());
1292 }
1293
1294 // Install plugin
1295 $skin = new \WP_Ajax_Upgrader_Skin();
1296 $upgrader = new \Plugin_Upgrader($skin);
1297 $result = $upgrader->install($api->download_link);
1298
1299 if (is_wp_error($result)) {
1300 wp_send_json_error($result->get_error_message());
1301 }
1302
1303 if ($result === false) {
1304 wp_send_json_error(__('Plugin installation failed.', 'master-addons'));
1305 }
1306
1307 wp_send_json_success([
1308 'message' => __('Plugin installed successfully!', 'master-addons'),
1309 'next_action' => 'activate',
1310 ]);
1311
1312 } elseif ($plugin_action === 'activate') {
1313 // Check permission
1314 if (!current_user_can('activate_plugins')) {
1315 wp_send_json_error(__('You do not have permission to activate plugins.', 'master-addons'));
1316 }
1317
1318 // Include plugin functions
1319 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1320
1321 // Check if plugin exists
1322 if (!file_exists(WP_PLUGIN_DIR . '/' . $plugin_file)) {
1323 wp_send_json_error(__('Plugin file not found.', 'master-addons'));
1324 }
1325
1326 // Activate plugin
1327 $result = activate_plugin($plugin_file);
1328
1329 if (is_wp_error($result)) {
1330 wp_send_json_error($result->get_error_message());
1331 }
1332
1333 wp_send_json_success([
1334 'message' => __('Plugin activated successfully!', 'master-addons'),
1335 'next_action' => 'done',
1336 ]);
1337 }
1338
1339 wp_send_json_error(__('Invalid action.', 'master-addons'));
1340 }
1341
1342 public function jltma_admin_notice_missing_main_plugin()
1343 {
1344 $plugin = 'elementor/elementor.php';
1345
1346 if ($this->is_elementor_activated()) {
1347 if (!current_user_can('activate_plugins')) {
1348 return;
1349 }
1350 $title = __('Elementor is Not Activated', 'master-addons');
1351 $message = __('Master Addons requires Elementor plugin to be active. Please activate Elementor to continue.', 'master-addons');
1352 $button_text = __('Activate Elementor', 'master-addons');
1353 $notice_type = 'success';
1354 $ajax_action = 'activate';
1355 } else {
1356 if (!current_user_can('install_plugins')) {
1357 return;
1358 }
1359 $title = __('Elementor is Not Installed', 'master-addons');
1360 $message = __('Master Addons requires Elementor plugin to be installed and activated. Please install Elementor to continue.', 'master-addons');
1361 $button_text = __('Install Elementor', 'master-addons');
1362 $notice_type = 'warning';
1363 $ajax_action = 'install';
1364 }
1365
1366 $this->jltma_render_ajax_plugin_notice($title, $message, $button_text, $notice_type, 'elementor', $ajax_action);
1367 }
1368
1369 public function jltma_admin_notice_minimum_elementor_version()
1370 {
1371 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of the WordPress-set "activate" query var to suppress the default activation notice; not form processing.
1372 if (isset($_GET['activate'])) {
1373 unset($_GET['activate']);
1374 }
1375
1376 $title = __('Elementor Version Update Required', 'master-addons');
1377 $message = sprintf(
1378 /* translators: 1: Required Elementor version */
1379 __('Master Addons requires Elementor version %s or greater. Please update Elementor to continue.', 'master-addons'),
1380 self::MINIMUM_ELEMENTOR_VERSION
1381 );
1382
1383 $this->jltma_render_elementor_style_notice($title, $message, '', '', 'warning');
1384 }
1385
1386 public function jltma_admin_notice_minimum_php_version()
1387 {
1388 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of the WordPress-set "activate" query var to suppress the default activation notice; not form processing.
1389 if (isset($_GET['activate'])) {
1390 unset($_GET['activate']);
1391 }
1392
1393 $title = __('PHP Version Update Required', 'master-addons');
1394 $message = sprintf(
1395 /* translators: 1: Required PHP version */
1396 __('Master Addons requires PHP version %s or greater. Please contact your hosting provider to upgrade PHP.', 'master-addons'),
1397 self::MINIMUM_PHP_VERSION
1398 );
1399
1400 $this->jltma_render_elementor_style_notice($title, $message, '', '', 'error');
1401 }
1402
1403 /**
1404 * Render Elementor-style admin notice
1405 *
1406 * @param string $title Notice title
1407 * @param string $message Notice message
1408 * @param string $button_url Optional button URL
1409 * @param string $button_text Optional button text
1410 * @param string $type Notice type: 'info', 'warning', 'error', 'success'
1411 */
1412 private function jltma_render_elementor_style_notice($title, $message, $button_url = '', $button_text = '', $type = 'info')
1413 {
1414 ?>
1415 <style>
1416 .jltma-notice{--jltma-notice-color:#5046e5;--jltma-notice-color-dark:#3f35c5;--jltma-notice-tint:#eeedfc;position:relative;display:flex;font-family:Roboto,Arial,Helvetica,sans-serif;background:#fff;border:1px solid #ccd0d4;border-left-width:4px;box-shadow:0 1px 4px rgba(0,0,0,.15);margin:5px 0 15px 0;padding:0;padding-right:30px;clear:both}
1417 .jltma-notice--jltma-warning{--jltma-notice-color:#f0a93b;--jltma-notice-color-dark:#d89a2f;--jltma-notice-tint:#fef8ee}
1418 .jltma-notice--jltma-error{--jltma-notice-color:#d63638;--jltma-notice-color-dark:#b32d2e;--jltma-notice-tint:#fcf0f1}
1419 .jltma-notice--jltma-success{--jltma-notice-color:#00a32a;--jltma-notice-color-dark:#008a20;--jltma-notice-tint:#edfaef}
1420 .jltma-notice::before{display:block;content:"";position:absolute;left:-4px;top:-1px;bottom:-1px;width:4px;background-color:var(--jltma-notice-color)}
1421 .jltma-notice__aside{overflow:hidden;background-color:var(--jltma-notice-tint);width:50px;text-align:center;padding-top:15px;flex-grow:0;flex-shrink:0}
1422 .jltma-notice__icon{display:inline-block;width:24px;height:24px;line-height:24px;border-radius:50%;overflow:hidden}
1423 .jltma-notice__icon img{width:24px;height:24px;border-radius:50%}
1424 .jltma-notice__content{padding:20px;flex:1}
1425 .jltma-notice__content h3{font-size:1.0625rem;font-weight:600;line-height:1.2;margin:0;color:#1e1e1e}
1426 .jltma-notice__content p{font-size:13px;font-weight:400;line-height:1.4;margin:8px 0 0 0;padding:0;color:#50575e}
1427 .jltma-notice__actions{display:flex;margin-top:1rem}
1428 .jltma-notice__actions>*+*{margin-left:8px}
1429 .jltma-notice__btn{display:inline-block;padding:8px 16px;font-size:13px;font-weight:500;line-height:1;text-decoration:none;color:#fff;background-color:var(--jltma-notice-color);border:none;border-radius:3px;cursor:pointer;transition:background-color .2s ease}
1430 .jltma-notice__btn:hover,.jltma-notice__btn:focus{background-color:var(--jltma-notice-color-dark);color:#fff;outline:none;box-shadow:none}
1431 .jltma-notice__dismiss{position:absolute;top:10px;right:10px;width:20px;height:20px;padding:0;background:none;border:none;cursor:pointer;line-height:1}
1432 .jltma-notice__dismiss::before{font-family:dashicons;content:"\f335";font-size:20px;color:#787c82;font-weight:400}
1433 .jltma-notice__dismiss:hover::before{color:#d63638}
1434 .jltma-notice__dismiss:focus{outline:none}
1435 </style>
1436 <div class="jltma-notice<?php echo $type !== 'info' ? ' jltma-notice--jltma-' . esc_attr($type) : ''; ?>">
1437 <div class="jltma-notice__aside">
1438 <div class="jltma-notice__icon">
1439 <img src="<?php echo esc_url(JLTMA_IMAGE_DIR . 'logo.svg'); ?>" alt="Master Addons">
1440 </div>
1441 </div>
1442 <div class="jltma-notice__content">
1443 <h3><?php echo esc_html($title); ?></h3>
1444 <p><?php echo esc_html($message); ?></p>
1445 <?php if (!empty($button_url) && !empty($button_text)) : ?>
1446 <div class="jltma-notice__actions">
1447 <a href="<?php echo esc_url($button_url); ?>" class="jltma-notice__btn"><?php echo esc_html($button_text); ?></a>
1448 </div>
1449 <?php endif; ?>
1450 </div>
1451 <button type="button" class="jltma-notice__dismiss" onclick="this.parentElement.remove();"></button>
1452 </div>
1453 <?php
1454 }
1455
1456 /**
1457 * Render AJAX-enabled plugin install/activate notice
1458 *
1459 * @param string $title Notice title
1460 * @param string $message Notice message
1461 * @param string $button_text Button text
1462 * @param string $type Notice type
1463 * @param string $plugin_slug Plugin slug (e.g., 'elementor')
1464 * @param string $action 'install' or 'activate'
1465 */
1466 private function jltma_render_ajax_plugin_notice($title, $message, $button_text, $type, $plugin_slug, $action)
1467 {
1468 $nonce = wp_create_nonce('jltma_plugin_action');
1469 ?>
1470 <style>
1471 .jltma-notice{--jltma-notice-color:#5046e5;--jltma-notice-color-dark:#3f35c5;--jltma-notice-tint:#eeedfc;position:relative;display:flex;font-family:Roboto,Arial,Helvetica,sans-serif;background:#fff;border:1px solid #ccd0d4;border-left-width:4px;box-shadow:0 1px 4px rgba(0,0,0,.15);margin:5px 0 15px 0;padding:0;padding-right:30px;clear:both}
1472 .jltma-notice--jltma-warning{--jltma-notice-color:#f0a93b;--jltma-notice-color-dark:#d89a2f;--jltma-notice-tint:#fef8ee}
1473 .jltma-notice--jltma-error{--jltma-notice-color:#d63638;--jltma-notice-color-dark:#b32d2e;--jltma-notice-tint:#fcf0f1}
1474 .jltma-notice--jltma-success{--jltma-notice-color:#00a32a;--jltma-notice-color-dark:#008a20;--jltma-notice-tint:#edfaef}
1475 .jltma-notice::before{display:block;content:"";position:absolute;left:-4px;top:-1px;bottom:-1px;width:4px;background-color:var(--jltma-notice-color)}
1476 .jltma-notice__aside{overflow:hidden;background-color:var(--jltma-notice-tint);width:50px;text-align:center;padding-top:15px;flex-grow:0;flex-shrink:0}
1477 .jltma-notice__icon{display:inline-block;width:24px;height:24px;line-height:24px;border-radius:50%;overflow:hidden}
1478 .jltma-notice__icon img{width:24px;height:24px;border-radius:50%}
1479 .jltma-notice__content{padding:20px;flex:1}
1480 .jltma-notice__content h3{font-size:1.0625rem;font-weight:600;line-height:1.2;margin:0;color:#1e1e1e}
1481 .jltma-notice__content p{font-size:13px;font-weight:400;line-height:1.4;margin:8px 0 0 0;padding:0;color:#50575e}
1482 .jltma-notice__actions{display:flex;align-items:center;margin-top:1rem;gap:10px}
1483 .jltma-notice__btn{display:inline-flex;align-items:center;gap:8px;padding:8px 16px;font-size:13px;font-weight:500;line-height:1;text-decoration:none;color:#fff;background-color:var(--jltma-notice-color);border:none;border-radius:3px;cursor:pointer;transition:all .2s ease}
1484 .jltma-notice__btn:hover,.jltma-notice__btn:focus{background-color:var(--jltma-notice-color-dark);color:#fff;outline:none;box-shadow:none}
1485 .jltma-notice__btn:disabled{opacity:0.7;cursor:not-allowed}
1486 .jltma-notice__btn .jltma-spinner{display:none;width:14px;height:14px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:jltma-spin 0.8s linear infinite}
1487 .jltma-notice__btn.is-loading .jltma-spinner{display:inline-block}
1488 .jltma-notice__btn.is-loading .jltma-btn-text{opacity:0.8}
1489 .jltma-notice__status{font-size:13px;color:#50575e}
1490 .jltma-notice__status.success{color:#00a32a}
1491 .jltma-notice__status.error{color:#d63638}
1492 @keyframes jltma-spin{to{transform:rotate(360deg)}}
1493 .jltma-notice__dismiss{position:absolute;top:10px;right:10px;width:20px;height:20px;padding:0;background:none;border:none;cursor:pointer;line-height:1}
1494 .jltma-notice__dismiss::before{font-family:dashicons;content:"\f335";font-size:20px;color:#787c82;font-weight:400}
1495 .jltma-notice__dismiss:hover::before{color:#d63638}
1496 .jltma-notice__dismiss:focus{outline:none}
1497 </style>
1498 <div class="jltma-notice<?php echo $type !== 'info' ? ' jltma-notice--jltma-' . esc_attr($type) : ''; ?>" id="jltma-plugin-notice">
1499 <div class="jltma-notice__aside">
1500 <div class="jltma-notice__icon">
1501 <img src="<?php echo esc_url(JLTMA_IMAGE_DIR . 'logo.svg'); ?>" alt="Master Addons">
1502 </div>
1503 </div>
1504 <div class="jltma-notice__content">
1505 <h3><?php echo esc_html($title); ?></h3>
1506 <p><?php echo esc_html($message); ?></p>
1507 <div class="jltma-notice__actions">
1508 <button type="button"
1509 class="jltma-notice__btn jltma-ajax-plugin-btn"
1510 data-plugin="<?php echo esc_attr($plugin_slug); ?>"
1511 data-action="<?php echo esc_attr($action); ?>"
1512 data-nonce="<?php echo esc_attr($nonce); ?>">
1513 <span class="jltma-spinner"></span>
1514 <span class="jltma-btn-text"><?php echo esc_html($button_text); ?></span>
1515 </button>
1516 <span class="jltma-notice__status"></span>
1517 </div>
1518 </div>
1519 <button type="button" class="jltma-notice__dismiss" onclick="this.parentElement.remove();"></button>
1520 </div>
1521 <script>
1522 (function() {
1523 var btn = document.querySelector('.jltma-ajax-plugin-btn');
1524 if (!btn) return;
1525
1526 btn.addEventListener('click', function(e) {
1527 e.preventDefault();
1528
1529 var button = this;
1530 var notice = document.getElementById('jltma-plugin-notice');
1531 var statusEl = notice.querySelector('.jltma-notice__status');
1532 var plugin = button.dataset.plugin;
1533 var action = button.dataset.action;
1534 var nonce = button.dataset.nonce;
1535 var btnText = button.querySelector('.jltma-btn-text');
1536 var originalText = btnText.textContent;
1537
1538 // Disable button and show loading
1539 button.disabled = true;
1540 button.classList.add('is-loading');
1541 statusEl.textContent = '';
1542 statusEl.className = 'jltma-notice__status';
1543
1544 if (action === 'install') {
1545 btnText.textContent = '<?php echo esc_js(__('Installing...', 'master-addons')); ?>';
1546 } else {
1547 btnText.textContent = '<?php echo esc_js(__('Activating...', 'master-addons')); ?>';
1548 }
1549
1550 // Make AJAX request
1551 var formData = new FormData();
1552 formData.append('action', 'jltma_plugin_action');
1553 formData.append('plugin_action', action);
1554 formData.append('plugin', plugin);
1555 formData.append('nonce', nonce);
1556
1557 fetch(ajaxurl, {
1558 method: 'POST',
1559 body: formData,
1560 credentials: 'same-origin'
1561 })
1562 .then(function(response) {
1563 return response.json();
1564 })
1565 .then(function(data) {
1566 button.classList.remove('is-loading');
1567
1568 if (data.success) {
1569 statusEl.textContent = data.data.message;
1570 statusEl.classList.add('success');
1571
1572 // If installed, now activate
1573 if (action === 'install' && data.data.next_action === 'activate') {
1574 button.dataset.action = 'activate';
1575 btnText.textContent = '<?php echo esc_js(__('Activate Elementor', 'master-addons')); ?>';
1576 button.disabled = false;
1577 statusEl.textContent = '<?php echo esc_js(__('Installed! Click to activate.', 'master-addons')); ?>';
1578 } else {
1579 // All done, reload page
1580 btnText.textContent = '<?php echo esc_js(__('Reloading...', 'master-addons')); ?>';
1581 setTimeout(function() {
1582 window.location.reload();
1583 }, 1000);
1584 }
1585 } else {
1586 statusEl.textContent = data.data || '<?php echo esc_js(__('An error occurred.', 'master-addons')); ?>';
1587 statusEl.classList.add('error');
1588 btnText.textContent = originalText;
1589 button.disabled = false;
1590 }
1591 })
1592 .catch(function(error) {
1593 button.classList.remove('is-loading');
1594 statusEl.textContent = '<?php echo esc_js(__('Connection error. Please try again.', 'master-addons')); ?>';
1595 statusEl.classList.add('error');
1596 btnText.textContent = originalText;
1597 button.disabled = false;
1598 });
1599 });
1600 })();
1601 </script>
1602 <?php
1603 }
1604
1605 // WordPress.org-hosted plugins have their translations loaded
1606 // automatically by WordPress (just-in-time, since 4.6), so a manual
1607 // load_plugin_textdomain() call is no longer required.
1608 public function load_textdomain() {}
1609
1610 // Check if Master Addons Pro is activated
1611 public function is_master_addons_pro_activated($plugin_path = 'master-addons-pro/master-addons.php')
1612 {
1613 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1614 return is_plugin_active($plugin_path);
1615 }
1616 }
1617 }
1618