PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.2.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.2.2
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / 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.2.2, at class-master-elementor-addons.php

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